save data
This commit is contained in:
parent
dad4ed7591
commit
791349a2c4
21 changed files with 711 additions and 349 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,4 +2,4 @@ node_modules
|
||||||
dist
|
dist
|
||||||
target
|
target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/export
|
**/export/export.json
|
132
Cargo.lock
generated
132
Cargo.lock
generated
|
@ -86,6 +86,55 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.86"
|
version = "1.0.86"
|
||||||
|
@ -410,6 +459,20 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backoff"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"getrandom 0.2.15",
|
||||||
|
"instant",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.71"
|
version = "0.3.71"
|
||||||
|
@ -446,6 +509,12 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bimap"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
|
@ -748,6 +817,46 @@ dependencies = [
|
||||||
"phf_codegen 0.11.2",
|
"phf_codegen 0.11.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.66",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cocoa"
|
name = "cocoa"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
@ -788,6 +897,12 @@ dependencies = [
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
|
@ -2454,6 +2569,12 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -3498,6 +3619,8 @@ dependencies = [
|
||||||
name = "panorama"
|
name = "panorama"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"panorama-daemon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
@ -3506,6 +3629,7 @@ dependencies = [
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3513,6 +3637,8 @@ name = "panorama-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-imap",
|
"async-imap",
|
||||||
|
"backoff",
|
||||||
|
"bimap",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cozo",
|
"cozo",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -6186,6 +6312,12 @@ version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utoipa"
|
name = "utoipa"
|
||||||
version = "5.0.0-alpha.0"
|
version = "5.0.0-alpha.0"
|
||||||
|
|
|
@ -18,14 +18,17 @@ crate-type = [
|
||||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.0.0-beta", features = [] }
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
|
panorama-daemon = { path = "../../crates/panorama-daemon" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tauri = { version = "2.0.0-beta", features = [] }
|
||||||
tauri-build = { version = "2.0.0-beta.17", features = ["config-toml"] }
|
tauri-build = { version = "2.0.0-beta.17", features = ["config-toml"] }
|
||||||
tauri-plugin-http = "2.0.0-beta.9"
|
tauri-plugin-http = "2.0.0-beta.9"
|
||||||
tauri-plugin-shell = "2.0.0-beta.7"
|
tauri-plugin-shell = "2.0.0-beta.7"
|
||||||
tauri-plugin-single-instance = "2.0.0-beta.9"
|
tauri-plugin-single-instance = "2.0.0-beta.9"
|
||||||
tauri-plugin-window-state = "2.0.0-beta"
|
tauri-plugin-window-state = "2.0.0-beta"
|
||||||
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
|
|
|
@ -1,5 +1,34 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
use clap::{Parser, Subcommand};
|
||||||
app_lib::run();
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Opt {
|
||||||
|
#[clap(long = "no-embedded-daemon")]
|
||||||
|
no_embedded_daemon: bool,
|
||||||
|
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Command {
|
||||||
|
Daemon,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let opt = Opt::parse();
|
||||||
|
|
||||||
|
match opt.command {
|
||||||
|
Some(Command::Daemon) => {
|
||||||
|
panorama_daemon::run().await;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if !opt.no_embedded_daemon {
|
||||||
|
tokio::spawn(panorama_daemon::run());
|
||||||
|
}
|
||||||
|
app_lib::run();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,31 @@
|
||||||
color: rgb(0, 0, 0, 0.5);
|
color: rgb(0, 0, 0, 0.5);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.newNodeMenu {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.25);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,24 @@
|
||||||
import styles from "./Header.module.scss";
|
import styles from "./Header.module.scss";
|
||||||
import NoteAddIcon from "@mui/icons-material/NoteAdd";
|
import NoteAddIcon from "@mui/icons-material/NoteAdd";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
|
||||||
import ListIcon from "@mui/icons-material/List";
|
import ListIcon from "@mui/icons-material/List";
|
||||||
|
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { sidebarExpandedAtom } from "./Sidebar";
|
import { sidebarExpandedAtom } from "./Sidebar";
|
||||||
import { useNodeControls } from "../App";
|
import { useNodeControls } from "../App";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import {
|
||||||
|
FloatingOverlay,
|
||||||
|
FloatingPortal,
|
||||||
|
autoUpdate,
|
||||||
|
offset,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useFocus,
|
||||||
|
useInteractions,
|
||||||
|
} from "@floating-ui/react";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { openNode } = useNodeControls();
|
const { openNode } = useNodeControls();
|
||||||
|
@ -17,6 +28,7 @@ export default function Header() {
|
||||||
queryFn: getVersion,
|
queryFn: getVersion,
|
||||||
});
|
});
|
||||||
const { data: version } = versionData;
|
const { data: version } = versionData;
|
||||||
|
console.log("version", version);
|
||||||
|
|
||||||
const createNewJournalPage = useCallback(() => {
|
const createNewJournalPage = useCallback(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -50,12 +62,72 @@ export default function Header() {
|
||||||
<span className={styles.title}>Panorama</span>
|
<span className={styles.title}>Panorama</span>
|
||||||
<span className={styles.version}>v{version}</span>
|
<span className={styles.version}>v{version}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={createNewJournalPage}>
|
<div>
|
||||||
<NoteAddIcon fontSize="inherit" />
|
<button type="button" onClick={createNewJournalPage}>
|
||||||
</button>
|
<NoteAddIcon fontSize="inherit" />
|
||||||
|
</button>
|
||||||
|
<NewNodeButton />
|
||||||
|
</div>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerBorder} />
|
<div className={styles.headerBorder} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NewNodeButton() {
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const { refs, context, floatingStyles } = useFloating({
|
||||||
|
placement: "bottom-start",
|
||||||
|
open: showMenu,
|
||||||
|
onOpenChange: setShowMenu,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
// middleware: [offset(10)],
|
||||||
|
});
|
||||||
|
const focus = useFocus(context);
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
focus,
|
||||||
|
useDismiss(context),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMenu((p) => !p)}
|
||||||
|
ref={refs.setReference}
|
||||||
|
>
|
||||||
|
<ArrowDropDownIcon fontSize="inherit" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showMenu && (
|
||||||
|
<FloatingPortal>
|
||||||
|
<FloatingOverlay>
|
||||||
|
<div
|
||||||
|
ref={refs.setFloating}
|
||||||
|
style={{ ...floatingStyles }}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
<NewNodeMenu />
|
||||||
|
</div>
|
||||||
|
</FloatingOverlay>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewNodeMenu() {
|
||||||
|
return (
|
||||||
|
<div className={styles.newNodeMenu}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<button type="button">Journal Page</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button">Media resource</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -105,8 +105,8 @@ function SearchMenu({ results }) {
|
||||||
openNode(result.node_id);
|
openNode(result.node_id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.title}>{result.title}</div>
|
{/* <div className={styles.title}>{result.title}</div> */}
|
||||||
<div className={styles.subtitle}>{result.content}</div>
|
<div className={styles.subtitle}>{JSON.stringify(result)}</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -8,19 +8,24 @@ import rehypeKatex from "rehype-katex";
|
||||||
import { parse as parseDate, format as formatDate } from "date-fns";
|
import { parse as parseDate, format as formatDate } from "date-fns";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
|
|
||||||
|
const JOURNAL_PAGE_CONTENT_FIELD_NAME = "panorama/journal/page/content";
|
||||||
|
|
||||||
export interface JournalPageProps {
|
export interface JournalPageProps {
|
||||||
id: string;
|
id: string;
|
||||||
data: {
|
data: {
|
||||||
day?: string;
|
day?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
fields: object;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function JournalPage({ id, data }: JournalPageProps) {
|
export default function JournalPage({ id, data }: JournalPageProps) {
|
||||||
const { day } = data;
|
const { day } = data;
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [value, setValue] = useState(() => data.content);
|
const [value, setValue] = useState(
|
||||||
|
() => data?.fields?.[JOURNAL_PAGE_CONTENT_FIELD_NAME],
|
||||||
|
);
|
||||||
const [valueToSave] = useDebounce(value, 1000, {
|
const [valueToSave] = useDebounce(value, 1000, {
|
||||||
leading: true,
|
leading: true,
|
||||||
trailing: true,
|
trailing: true,
|
||||||
|
|
|
@ -4,6 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
backoff = { version = "0.4.0", features = ["tokio"] }
|
||||||
|
bimap = "0.6.3"
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
|
cozo = { version = "0.7.6", features = ["storage-rocksdb"] }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
|
|
|
@ -8,6 +8,7 @@ extern crate sugars;
|
||||||
pub mod migrations;
|
pub mod migrations;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
|
pub mod mail;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ use serde_json::Value;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
pub struct NodeId(Uuid);
|
pub struct NodeId(pub Uuid);
|
||||||
|
|
||||||
impl fmt::Display for NodeId {
|
impl fmt::Display for NodeId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
|
160
crates/panorama-core/src/mail.rs
Normal file
160
crates/panorama-core/src/mail.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
use backoff::{exponential::ExponentialBackoff, SystemClock};
|
||||||
|
use cozo::{DataValue, JsonData, ScriptMutability};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
use tokio::{net::TcpStream, time::sleep};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub async fn mail_loop(state: AppState) -> Result<()> {
|
||||||
|
backoff::future::retry(
|
||||||
|
ExponentialBackoff::<SystemClock>::default(),
|
||||||
|
|| async {
|
||||||
|
mail_loop_inner(&state).await?;
|
||||||
|
// For now, just sleep 30 seconds and then fetch again
|
||||||
|
// TODO: Run a bunch of connections at once and do IDLE over them (if possible)
|
||||||
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn mail_loop_inner(state: &AppState) -> Result<()> {
|
||||||
|
// Fetch the mail configs
|
||||||
|
let configs = state.fetch_mail_configs()?;
|
||||||
|
if configs.len() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do all configs instead of just the first
|
||||||
|
let config = &configs[0];
|
||||||
|
|
||||||
|
let stream =
|
||||||
|
TcpStream::connect((config.imap_hostname.as_str(), config.imap_port))
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
let client = async_imap::Client::new(stream);
|
||||||
|
let mut session = client
|
||||||
|
.login(&config.imap_username, &config.imap_password)
|
||||||
|
.await
|
||||||
|
.map_err(|(err, _)| err)
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
// println!("Session: {:?}", session);
|
||||||
|
let mailboxes = session
|
||||||
|
.list(None, Some("*"))
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
let mailbox_names =
|
||||||
|
mailboxes.iter().map(|name| name.name()).collect::<Vec<_>>();
|
||||||
|
println!("mailboxes: {mailbox_names:?}");
|
||||||
|
|
||||||
|
// Get the mailbox with INBOX
|
||||||
|
let inbox_node_id = {
|
||||||
|
let result = state.db.run_script("
|
||||||
|
?[node_id] :=
|
||||||
|
*mailbox{node_id, account_node_id, mailbox_name},
|
||||||
|
account_node_id = $account_node_id,
|
||||||
|
mailbox_name = 'INBOX'
|
||||||
|
", btmap! {"account_node_id".to_owned()=>DataValue::from(config.node_id.to_string())}, ScriptMutability::Immutable)?;
|
||||||
|
|
||||||
|
if result.rows.len() == 0 {
|
||||||
|
let new_node_id = Uuid::now_v7();
|
||||||
|
let new_node_id = new_node_id.to_string();
|
||||||
|
state.db.run_script("
|
||||||
|
?[node_id, account_node_id, mailbox_name] <-
|
||||||
|
[[$new_node_id, $account_node_id, 'INBOX']]
|
||||||
|
:put mailbox { node_id, account_node_id, mailbox_name }
|
||||||
|
",
|
||||||
|
btmap! {
|
||||||
|
"new_node_id".to_owned() => DataValue::from(new_node_id.clone()),
|
||||||
|
"account_node_id".to_owned() => DataValue::from(config.node_id.to_string()),
|
||||||
|
},
|
||||||
|
ScriptMutability::Mutable)?;
|
||||||
|
new_node_id
|
||||||
|
} else {
|
||||||
|
result.rows[0][0].get_str().unwrap().to_owned()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("INBOX: {:?}", inbox_node_id);
|
||||||
|
|
||||||
|
let inbox = session.select("INBOX").await.into_diagnostic()?;
|
||||||
|
println!("last unseen: {:?}", inbox.unseen);
|
||||||
|
|
||||||
|
let messages = session
|
||||||
|
.fetch(
|
||||||
|
"1:4",
|
||||||
|
"(FLAGS ENVELOPE BODY[HEADER] BODY[TEXT] INTERNALDATE)",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
println!(
|
||||||
|
"messages {:?}",
|
||||||
|
messages.iter().map(|f| f.body()).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
let input_data = DataValue::List(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.map(|msg| {
|
||||||
|
let message_id = Uuid::now_v7();
|
||||||
|
let headers =
|
||||||
|
String::from_utf8(msg.header().unwrap().to_vec()).unwrap();
|
||||||
|
let headers = headers
|
||||||
|
.split("\r\n")
|
||||||
|
.filter_map(|s| {
|
||||||
|
let p = s.split(": ").collect::<Vec<_>>();
|
||||||
|
if p.len() < 2 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((p[0], p[1]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
DataValue::List(vec![
|
||||||
|
DataValue::from(message_id.to_string()),
|
||||||
|
DataValue::from(config.node_id.to_string()),
|
||||||
|
DataValue::from(inbox_node_id.clone()),
|
||||||
|
DataValue::from(
|
||||||
|
headers
|
||||||
|
.get("Subject")
|
||||||
|
.map(|s| (*s).to_owned())
|
||||||
|
.unwrap_or("Subject".to_owned()),
|
||||||
|
),
|
||||||
|
DataValue::Json(JsonData(serde_json::to_value(headers).unwrap())),
|
||||||
|
DataValue::Bytes(msg.text().unwrap().to_vec()),
|
||||||
|
DataValue::from(msg.internal_date().unwrap().to_rfc3339()),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.db.run_script(
|
||||||
|
"
|
||||||
|
?[node_id, account_node_id, mailbox_node_id, subject, headers, body, internal_date] <- $input_data
|
||||||
|
:put message { node_id, account_node_id, mailbox_node_id, subject, headers, body, internal_date }
|
||||||
|
",
|
||||||
|
btmap! {
|
||||||
|
"input_data".to_owned() => input_data,
|
||||||
|
},
|
||||||
|
ScriptMutability::Mutable,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
session.logout().await.into_diagnostic()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -136,6 +136,9 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
['panorama/mail/config/imap_port', 'mail_config', 'imap_port', 'int', false],
|
['panorama/mail/config/imap_port', 'mail_config', 'imap_port', 'int', false],
|
||||||
['panorama/mail/config/imap_username', 'mail_config', 'imap_username', 'string', false],
|
['panorama/mail/config/imap_username', 'mail_config', 'imap_username', 'string', false],
|
||||||
['panorama/mail/config/imap_password', 'mail_config', 'imap_password', 'string', false],
|
['panorama/mail/config/imap_password', 'mail_config', 'imap_password', 'string', false],
|
||||||
|
['panorama/mail/message/body', 'message', 'body', 'string', true],
|
||||||
|
['panorama/mail/message/subject', 'message', 'subject', 'string', true],
|
||||||
|
['panorama/mail/message/message_id', 'message', 'message_id', 'string', true],
|
||||||
]
|
]
|
||||||
:put fqkey_to_dbkey { key, relation, field_name, type, is_fts_enabled }
|
:put fqkey_to_dbkey { key, relation, field_name, type, is_fts_enabled }
|
||||||
}
|
}
|
||||||
|
@ -143,14 +146,6 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
# Create journal type
|
# Create journal type
|
||||||
{ :create journal { node_id: String => content: String } }
|
{ :create journal { node_id: String => content: String } }
|
||||||
{ :create journal_day { day: String => node_id: String } }
|
{ :create journal_day { day: String => node_id: String } }
|
||||||
{
|
|
||||||
::fts create journal:text_index {
|
|
||||||
extractor: content,
|
|
||||||
extract_filter: !is_null(content),
|
|
||||||
tokenizer: Simple,
|
|
||||||
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Mail
|
# Mail
|
||||||
{
|
{
|
||||||
|
@ -176,6 +171,7 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
:create message {
|
:create message {
|
||||||
node_id: String
|
node_id: String
|
||||||
=>
|
=>
|
||||||
|
message_id: String,
|
||||||
account_node_id: String,
|
account_node_id: String,
|
||||||
mailbox_node_id: String,
|
mailbox_node_id: String,
|
||||||
subject: String,
|
subject: String,
|
||||||
|
@ -184,6 +180,9 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
||||||
internal_date: String,
|
internal_date: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
{ ::index create message:message_id { message_id } }
|
||||||
|
{ ::index create message:date { internal_date } }
|
||||||
|
{ ::index create message:by_mailbox_id { mailbox_node_id } }
|
||||||
|
|
||||||
# Calendar
|
# Calendar
|
||||||
",
|
",
|
||||||
|
|
|
@ -6,6 +6,8 @@ use serde_json::Value;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
use super::utils::data_value_to_json_value;
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub async fn export(&self) -> Result<Value> {
|
pub async fn export(&self) -> Result<Value> {
|
||||||
let result = self.db.run_script(
|
let result = self.db.run_script(
|
||||||
|
@ -57,7 +59,8 @@ impl AppState {
|
||||||
for row in result.rows.into_iter() {
|
for row in result.rows.into_iter() {
|
||||||
let mut object = hmap! {};
|
let mut object = hmap! {};
|
||||||
row.into_iter().enumerate().for_each(|(idx, col)| {
|
row.into_iter().enumerate().for_each(|(idx, col)| {
|
||||||
object.insert(columns[idx].to_owned(), col);
|
object
|
||||||
|
.insert(columns[idx].to_owned(), data_value_to_json_value(&col));
|
||||||
});
|
});
|
||||||
relation_info.push(object);
|
relation_info.push(object);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ use crate::{AppState, NodeId};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct MailConfig {
|
pub struct MailConfig {
|
||||||
node_id: NodeId,
|
pub node_id: NodeId,
|
||||||
imap_hostname: String,
|
pub imap_hostname: String,
|
||||||
imap_port: u16,
|
pub imap_port: u16,
|
||||||
imap_username: String,
|
pub imap_username: String,
|
||||||
imap_password: String,
|
pub imap_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
@ -44,156 +44,4 @@ impl AppState {
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mail_loop(&self) {
|
|
||||||
loop {
|
|
||||||
match self.mail_loop_inner().await {
|
|
||||||
Ok(_) => {
|
|
||||||
// For now, just sleep 30 seconds and then fetch again
|
|
||||||
// TODO: Run a bunch of connections at once and do IDLE over them (if possible)
|
|
||||||
sleep(Duration::from_secs(30)).await;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Fetch config error: {err:?}");
|
|
||||||
// Back off, retry
|
|
||||||
// TODO: Exponential backoff
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mail_loop_inner(&self) -> Result<()> {
|
|
||||||
// Fetch the mail configs
|
|
||||||
let configs = self.fetch_mail_configs()?;
|
|
||||||
if configs.len() == 0 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Do all configs instead of just the first
|
|
||||||
let config = &configs[0];
|
|
||||||
|
|
||||||
let stream =
|
|
||||||
TcpStream::connect((config.imap_hostname.as_str(), config.imap_port))
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
|
|
||||||
let client = async_imap::Client::new(stream);
|
|
||||||
let mut session = client
|
|
||||||
.login(&config.imap_username, &config.imap_password)
|
|
||||||
.await
|
|
||||||
.map_err(|(err, _)| err)
|
|
||||||
.into_diagnostic()?;
|
|
||||||
|
|
||||||
// println!("Session: {:?}", session);
|
|
||||||
let mailboxes = session
|
|
||||||
.list(None, Some("*"))
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
let mailbox_names =
|
|
||||||
mailboxes.iter().map(|name| name.name()).collect::<Vec<_>>();
|
|
||||||
println!("mailboxes: {mailbox_names:?}");
|
|
||||||
|
|
||||||
// Get the mailbox with INBOX
|
|
||||||
let inbox_node_id = {
|
|
||||||
let result = self.db.run_script("
|
|
||||||
?[node_id] :=
|
|
||||||
*mailbox{node_id, account_node_id, mailbox_name},
|
|
||||||
account_node_id = $account_node_id,
|
|
||||||
mailbox_name = 'INBOX'
|
|
||||||
", btmap! {"account_node_id".to_owned()=>DataValue::from(config.node_id.to_string())}, ScriptMutability::Immutable)?;
|
|
||||||
|
|
||||||
if result.rows.len() == 0 {
|
|
||||||
let new_node_id = Uuid::now_v7();
|
|
||||||
let new_node_id = new_node_id.to_string();
|
|
||||||
self.db.run_script("
|
|
||||||
?[node_id, account_node_id, mailbox_name] <-
|
|
||||||
[[$new_node_id, $account_node_id, 'INBOX']]
|
|
||||||
:put mailbox { node_id, account_node_id, mailbox_name }
|
|
||||||
",
|
|
||||||
btmap! {
|
|
||||||
"new_node_id".to_owned() => DataValue::from(new_node_id.clone()),
|
|
||||||
"account_node_id".to_owned() => DataValue::from(config.node_id.to_string()),
|
|
||||||
},
|
|
||||||
ScriptMutability::Mutable)?;
|
|
||||||
new_node_id
|
|
||||||
} else {
|
|
||||||
result.rows[0][0].get_str().unwrap().to_owned()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
println!("INBOX: {:?}", inbox_node_id);
|
|
||||||
|
|
||||||
let inbox = session.select("INBOX").await.into_diagnostic()?;
|
|
||||||
println!("last unseen: {:?}", inbox.unseen);
|
|
||||||
|
|
||||||
let messages = session
|
|
||||||
.fetch(
|
|
||||||
"1:4",
|
|
||||||
"(FLAGS ENVELOPE BODY[HEADER] BODY[TEXT] INTERNALDATE)",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
println!(
|
|
||||||
"messages {:?}",
|
|
||||||
messages.iter().map(|f| f.body()).collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
let input_data = DataValue::List(
|
|
||||||
messages
|
|
||||||
.iter()
|
|
||||||
.map(|msg| {
|
|
||||||
let message_id = Uuid::now_v7();
|
|
||||||
let headers =
|
|
||||||
String::from_utf8(msg.header().unwrap().to_vec()).unwrap();
|
|
||||||
let headers = headers
|
|
||||||
.split("\r\n")
|
|
||||||
.filter_map(|s| {
|
|
||||||
let p = s.split(": ").collect::<Vec<_>>();
|
|
||||||
if p.len() < 2 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((p[0], p[1]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
DataValue::List(vec![
|
|
||||||
DataValue::from(message_id.to_string()),
|
|
||||||
DataValue::from(config.node_id.to_string()),
|
|
||||||
DataValue::from(inbox_node_id.clone()),
|
|
||||||
DataValue::from(
|
|
||||||
headers
|
|
||||||
.get("Subject")
|
|
||||||
.map(|s| (*s).to_owned())
|
|
||||||
.unwrap_or("Subject".to_owned()),
|
|
||||||
),
|
|
||||||
DataValue::Json(JsonData(serde_json::to_value(headers).unwrap())),
|
|
||||||
DataValue::Bytes(msg.text().unwrap().to_vec()),
|
|
||||||
DataValue::from(msg.internal_date().unwrap().to_rfc3339()),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.db.run_script(
|
|
||||||
"
|
|
||||||
?[node_id, account_node_id, mailbox_node_id, subject, headers, body, internal_date] <- $input_data
|
|
||||||
:put message { node_id, account_node_id, mailbox_node_id, subject, headers, body, internal_date }
|
|
||||||
",
|
|
||||||
btmap! {
|
|
||||||
"input_data".to_owned() => input_data,
|
|
||||||
},
|
|
||||||
ScriptMutability::Mutable,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
session.logout().await.into_diagnostic()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ pub mod export;
|
||||||
pub mod journal;
|
pub mod journal;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
pub mod node;
|
pub mod node;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
use std::{collections::HashMap, fs, path::Path};
|
use std::{collections::HashMap, fs, path::Path};
|
||||||
|
|
||||||
|
use bimap::BiMap;
|
||||||
use cozo::DbInstance;
|
use cozo::DbInstance;
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use tantivy::{
|
use tantivy::{
|
||||||
|
@ -13,12 +15,12 @@ use tantivy::{
|
||||||
Index,
|
Index,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::migrations::run_migrations;
|
use crate::{mail::mail_loop, migrations::run_migrations};
|
||||||
|
|
||||||
pub fn tantivy_schema() -> (Schema, HashMap<String, Field>) {
|
pub fn tantivy_schema() -> (Schema, BiMap<String, Field>) {
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
|
|
||||||
let mut field_map = HashMap::new();
|
let mut field_map = BiMap::new();
|
||||||
|
|
||||||
let node_id = schema_builder.add_text_field("node_id", STRING | STORED);
|
let node_id = schema_builder.add_text_field("node_id", STRING | STORED);
|
||||||
field_map.insert("node_id".to_owned(), node_id);
|
field_map.insert("node_id".to_owned(), node_id);
|
||||||
|
@ -33,7 +35,7 @@ pub fn tantivy_schema() -> (Schema, HashMap<String, Field>) {
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: DbInstance,
|
pub db: DbInstance,
|
||||||
pub tantivy_index: Index,
|
pub tantivy_index: Index,
|
||||||
pub tantivy_field_map: HashMap<String, Field>,
|
pub tantivy_field_map: BiMap<String, Field>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
@ -75,7 +77,7 @@ impl AppState {
|
||||||
run_migrations(&self.db).await?;
|
run_migrations(&self.db).await?;
|
||||||
|
|
||||||
let state = self.clone();
|
let state = self.clone();
|
||||||
tokio::spawn(async move { state.mail_loop().await });
|
tokio::spawn(async move { mail_loop(state).await });
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{AppState, NodeId};
|
use crate::{AppState, NodeId};
|
||||||
|
|
||||||
|
use super::utils::{data_value_to_json_value, owned_value_to_json_value};
|
||||||
|
|
||||||
pub type ExtraData = BTreeMap<String, Value>;
|
pub type ExtraData = BTreeMap<String, Value>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -25,7 +27,7 @@ pub struct NodeInfo {
|
||||||
pub node_id: NodeId,
|
pub node_id: NodeId,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub fields: Option<HashMap<String, DataValue>>,
|
pub fields: Option<HashMap<String, Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FieldInfo {
|
pub struct FieldInfo {
|
||||||
|
@ -142,7 +144,7 @@ impl AppState {
|
||||||
.map(|row| row.into_iter().skip(4).zip(all_fields.iter()))
|
.map(|row| row.into_iter().skip(4).zip(all_fields.iter()))
|
||||||
{
|
{
|
||||||
for (value, (_, _, field_name)) in row {
|
for (value, (_, _, field_name)) in row {
|
||||||
fields.insert(field_name.to_string(), value);
|
fields.insert(field_name.to_string(), data_value_to_json_value(&value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,35 +155,81 @@ impl AppState {
|
||||||
fields: Some(fields),
|
fields: Some(fields),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CreateOrUpdate {
|
||||||
|
Create { r#type: String },
|
||||||
|
Update { node_id: NodeId },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
// TODO: Split this out into create and update
|
// TODO: Split this out into create and update
|
||||||
pub async fn create_or_update_node(
|
pub async fn create_or_update_node(
|
||||||
&self,
|
&self,
|
||||||
r#type: impl AsRef<str>,
|
opts: CreateOrUpdate,
|
||||||
extra_data: Option<ExtraData>,
|
extra_data: Option<ExtraData>,
|
||||||
) -> Result<NodeInfo> {
|
) -> Result<NodeInfo> {
|
||||||
let ty = r#type.as_ref();
|
let node_id = match opts {
|
||||||
|
CreateOrUpdate::Create { .. } => NodeId(Uuid::now_v7()),
|
||||||
let node_id = Uuid::now_v7();
|
CreateOrUpdate::Update { ref node_id } => node_id.clone(),
|
||||||
|
};
|
||||||
let node_id = node_id.to_string();
|
let node_id = node_id.to_string();
|
||||||
|
|
||||||
let tx = self.db.multi_transaction(true);
|
let tx = self.db.multi_transaction(true);
|
||||||
|
|
||||||
let node_result = tx.run_script(
|
let (created_at, updated_at) = match opts {
|
||||||
"
|
CreateOrUpdate::Create { r#type } => {
|
||||||
|
let node_result = tx.run_script(
|
||||||
|
"
|
||||||
?[id, type] <- [[$node_id, $type]]
|
?[id, type] <- [[$node_id, $type]]
|
||||||
:put node { id, type }
|
:put node { id, type }
|
||||||
:returning
|
:returning
|
||||||
",
|
",
|
||||||
btmap! {
|
btmap! {
|
||||||
"node_id".to_owned() => DataValue::from(node_id.clone()),
|
"node_id".to_owned() => DataValue::from(node_id.clone()),
|
||||||
"type".to_owned() => DataValue::from(ty),
|
"type".to_owned() => DataValue::from(r#type),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
println!("ROWS(1): {:?}", node_result);
|
||||||
|
let created_at = DateTime::from_timestamp_millis(
|
||||||
|
(node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let updated_at = DateTime::from_timestamp_millis(
|
||||||
|
(node_result.rows[0][5].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()),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
println!("ROWS(2): {:?}", node_result);
|
||||||
|
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 {
|
if let Some(extra_data) = extra_data {
|
||||||
let node_id_field =
|
let node_id_field = self
|
||||||
self.tantivy_field_map.get("node_id").unwrap().clone();
|
.tantivy_field_map
|
||||||
|
.get_by_left("node_id")
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
if !extra_data.is_empty() {
|
if !extra_data.is_empty() {
|
||||||
let keys = extra_data.keys().map(|s| s.to_owned()).collect::<Vec<_>>();
|
let keys = extra_data.keys().map(|s| s.to_owned()).collect::<Vec<_>>();
|
||||||
let field_mapping =
|
let field_mapping =
|
||||||
|
@ -215,7 +263,8 @@ impl AppState {
|
||||||
};
|
};
|
||||||
|
|
||||||
if *is_fts_enabled {
|
if *is_fts_enabled {
|
||||||
if let Some(field) = self.tantivy_field_map.get(*key) {
|
if let Some(field) = self.tantivy_field_map.get_by_left(*key)
|
||||||
|
{
|
||||||
doc.insert(
|
doc.insert(
|
||||||
field.clone(),
|
field.clone(),
|
||||||
OwnedValue::Str(new_value.get_str().unwrap().to_owned()),
|
OwnedValue::Str(new_value.get_str().unwrap().to_owned()),
|
||||||
|
@ -240,7 +289,7 @@ impl AppState {
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"
|
"
|
||||||
?[ node_id, {keys_joined} ] <- [$input_data]
|
?[ node_id, {keys_joined} ] <- [$input_data]
|
||||||
:insert {relation} {{ node_id, {keys_joined} }}
|
:put {relation} {{ node_id, {keys_joined} }}
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -285,15 +334,6 @@ impl AppState {
|
||||||
|
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
|
||||||
let created_at = DateTime::from_timestamp_millis(
|
|
||||||
(node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let updated_at = DateTime::from_timestamp_millis(
|
|
||||||
(node_result.rows[0][5].get_float().unwrap() * 1000.0) as i64,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(NodeInfo {
|
Ok(NodeInfo {
|
||||||
node_id: NodeId(Uuid::from_str(&node_id).unwrap()),
|
node_id: NodeId(Uuid::from_str(&node_id).unwrap()),
|
||||||
created_at,
|
created_at,
|
||||||
|
@ -313,10 +353,14 @@ impl AppState {
|
||||||
let reader = self.tantivy_index.reader().into_diagnostic()?;
|
let reader = self.tantivy_index.reader().into_diagnostic()?;
|
||||||
let searcher = reader.searcher();
|
let searcher = reader.searcher();
|
||||||
|
|
||||||
let node_id_field = self.tantivy_field_map.get("node_id").unwrap().clone();
|
let node_id_field = self
|
||||||
|
.tantivy_field_map
|
||||||
|
.get_by_left("node_id")
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
let journal_page_field = self
|
let journal_page_field = self
|
||||||
.tantivy_field_map
|
.tantivy_field_map
|
||||||
.get("panorama/journal/page/content")
|
.get_by_left("panorama/journal/page/content")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone();
|
.clone();
|
||||||
let query_parser =
|
let query_parser =
|
||||||
|
@ -342,10 +386,19 @@ impl AppState {
|
||||||
let node_id = NodeId(Uuid::from_str(node_id).unwrap());
|
let node_id = NodeId(Uuid::from_str(node_id).unwrap());
|
||||||
let fields = all_fields
|
let fields = all_fields
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(field, value)| {
|
.map(|(field, values)| {
|
||||||
(
|
(
|
||||||
serde_json::to_string(&field).unwrap(),
|
self.tantivy_field_map.get_by_right(&field).unwrap(),
|
||||||
serde_json::to_string(&value).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::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
57
crates/panorama-core/src/state/utils.rs
Normal file
57
crates/panorama-core/src/state/utils.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use cozo::{DataValue, Num};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde_json::{Number, Value};
|
||||||
|
use tantivy::schema::OwnedValue;
|
||||||
|
|
||||||
|
pub fn owned_value_to_json_value(data_value: &OwnedValue) -> Value {
|
||||||
|
match data_value {
|
||||||
|
OwnedValue::Null => Value::Null,
|
||||||
|
OwnedValue::Str(s) => Value::String(s.to_string()),
|
||||||
|
OwnedValue::U64(u) => Value::Number(Number::from(*u)),
|
||||||
|
OwnedValue::I64(i) => Value::Number(Number::from(*i)),
|
||||||
|
OwnedValue::F64(f) => Value::Number(Number::from_f64(*f).unwrap()),
|
||||||
|
OwnedValue::Bool(b) => Value::Bool(*b),
|
||||||
|
OwnedValue::Array(a) => {
|
||||||
|
Value::Array(a.into_iter().map(owned_value_to_json_value).collect_vec())
|
||||||
|
}
|
||||||
|
OwnedValue::Object(o) => Value::Object(
|
||||||
|
o.into_iter()
|
||||||
|
.map(|(k, v)| (k.to_owned(), owned_value_to_json_value(v)))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
println!("Converting unknown {:?}", data_value);
|
||||||
|
serde_json::to_value(data_value).unwrap()
|
||||||
|
} // OwnedValue::Date(_) => todo!(),
|
||||||
|
// OwnedValue::Facet(_) => todo!(),
|
||||||
|
// OwnedValue::Bytes(_) => todo!(),
|
||||||
|
// OwnedValue::IpAddr(_) => todo!(),
|
||||||
|
// OwnedValue::PreTokStr(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_value_to_json_value(data_value: &DataValue) -> Value {
|
||||||
|
match data_value {
|
||||||
|
DataValue::Null => Value::Null,
|
||||||
|
DataValue::Bool(b) => Value::Bool(*b),
|
||||||
|
DataValue::Num(n) => Value::Number(match n {
|
||||||
|
Num::Int(i) => Number::from(*i),
|
||||||
|
Num::Float(f) => Number::from_f64(*f).unwrap(),
|
||||||
|
}),
|
||||||
|
DataValue::Str(s) => Value::String(s.to_string()),
|
||||||
|
DataValue::List(v) => {
|
||||||
|
Value::Array(v.into_iter().map(data_value_to_json_value).collect_vec())
|
||||||
|
}
|
||||||
|
DataValue::Json(v) => v.0.clone(),
|
||||||
|
_ => {
|
||||||
|
println!("Converting unknown {:?}", data_value);
|
||||||
|
serde_json::to_value(data_value).unwrap()
|
||||||
|
} // DataValue::Bytes(s) => todo!(),
|
||||||
|
// DataValue::Uuid(_) => todo!(),
|
||||||
|
// DataValue::Regex(_) => todo!(),
|
||||||
|
// DataValue::Set(_) => todo!(),
|
||||||
|
// DataValue::Vec(_) => todo!(),
|
||||||
|
// DataValue::Validity(_) => todo!(),
|
||||||
|
// DataValue::Bot => todo!(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ pub async fn test_create_node() -> Result<()> {
|
||||||
|
|
||||||
let node_info = state
|
let node_info = state
|
||||||
.create_or_update_node(
|
.create_or_update_node(
|
||||||
|
None,
|
||||||
"panorama/journal/page",
|
"panorama/journal/page",
|
||||||
Some(btmap! {
|
Some(btmap! {
|
||||||
"panorama/journal/page/content".to_owned() => json!("helloge"),
|
"panorama/journal/page/content".to_owned() => json!("helloge"),
|
||||||
|
|
72
crates/panorama-daemon/src/lib.rs
Normal file
72
crates/panorama-daemon/src/lib.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate anyhow;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde_json;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate sugars;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod export;
|
||||||
|
mod journal;
|
||||||
|
pub mod mail;
|
||||||
|
mod node;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use axum::{http::Method, routing::get, Router};
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
use panorama_core::AppState;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::cors::{self, CorsLayer};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_scalar::{Scalar, Servable};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
export::export,
|
||||||
|
mail::{get_mail, get_mail_config},
|
||||||
|
node::search_nodes,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn run() -> Result<()> {
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
modifiers(),
|
||||||
|
nest(
|
||||||
|
(path = "/journal", api = crate::journal::JournalApi),
|
||||||
|
(path = "/node", api = crate::node::NodeApi),
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
|
let data_dir = dirs::data_dir().unwrap();
|
||||||
|
let panorama_dir = data_dir.join("panorama");
|
||||||
|
fs::create_dir_all(&panorama_dir).into_diagnostic()?;
|
||||||
|
|
||||||
|
let state = AppState::new(&panorama_dir).await?;
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::PUT])
|
||||||
|
.allow_headers(cors::Any)
|
||||||
|
.allow_origin(cors::Any);
|
||||||
|
|
||||||
|
// build our application with a single route
|
||||||
|
let app = Router::new()
|
||||||
|
.merge(Scalar::with_url("/api/docs", ApiDoc::openapi()))
|
||||||
|
.route("/", get(|| async { "Hello, World!" }))
|
||||||
|
.route("/export", get(export))
|
||||||
|
.nest("/node", node::router().with_state(state.clone()))
|
||||||
|
.nest("/journal", journal::router().with_state(state.clone()))
|
||||||
|
.route("/mail/config", get(get_mail_config))
|
||||||
|
.route("/mail", get(get_mail))
|
||||||
|
.layer(ServiceBuilder::new().layer(cors))
|
||||||
|
.with_state(state.clone());
|
||||||
|
|
||||||
|
let listener = TcpListener::bind("0.0.0.0:5195").await.into_diagnostic()?;
|
||||||
|
println!("Listening... {:?}", listener);
|
||||||
|
axum::serve(listener, app).await.into_diagnostic()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,73 +1,7 @@
|
||||||
#[macro_use]
|
use miette::Result;
|
||||||
extern crate anyhow;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate sugars;
|
|
||||||
|
|
||||||
mod error;
|
|
||||||
mod export;
|
|
||||||
mod journal;
|
|
||||||
pub mod mail;
|
|
||||||
mod node;
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use axum::{http::Method, routing::get, Router};
|
|
||||||
use miette::{IntoDiagnostic, Result};
|
|
||||||
use panorama_core::AppState;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::cors::{self, CorsLayer};
|
|
||||||
use utoipa::OpenApi;
|
|
||||||
use utoipa_scalar::{Scalar, Servable};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
export::export,
|
|
||||||
mail::{get_mail, get_mail_config},
|
|
||||||
node::search_nodes,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
#[derive(OpenApi)]
|
panorama_daemon::run().await?;
|
||||||
#[openapi(
|
|
||||||
modifiers(),
|
|
||||||
nest(
|
|
||||||
(path = "/journal", api = crate::journal::JournalApi),
|
|
||||||
(path = "/node", api = crate::node::NodeApi),
|
|
||||||
),
|
|
||||||
)]
|
|
||||||
struct ApiDoc;
|
|
||||||
|
|
||||||
let data_dir = dirs::data_dir().unwrap();
|
|
||||||
let panorama_dir = data_dir.join("panorama");
|
|
||||||
fs::create_dir_all(&panorama_dir).into_diagnostic()?;
|
|
||||||
|
|
||||||
let state = AppState::new(&panorama_dir).await?;
|
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
|
||||||
.allow_methods([Method::GET, Method::POST, Method::PUT])
|
|
||||||
.allow_headers(cors::Any)
|
|
||||||
.allow_origin(cors::Any);
|
|
||||||
|
|
||||||
// build our application with a single route
|
|
||||||
let app = Router::new()
|
|
||||||
.merge(Scalar::with_url("/api/docs", ApiDoc::openapi()))
|
|
||||||
.route("/", get(|| async { "Hello, World!" }))
|
|
||||||
.route("/export", get(export))
|
|
||||||
.nest("/node", node::router().with_state(state.clone()))
|
|
||||||
.nest("/journal", journal::router().with_state(state.clone()))
|
|
||||||
.route("/mail/config", get(get_mail_config))
|
|
||||||
.route("/mail", get(get_mail))
|
|
||||||
.layer(ServiceBuilder::new().layer(cors))
|
|
||||||
.with_state(state.clone());
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:5195").await.into_diagnostic()?;
|
|
||||||
println!("Listening... {:?}", listener);
|
|
||||||
axum::serve(listener, app).await.into_diagnostic()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
|
@ -8,9 +11,15 @@ use axum::{
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use cozo::{DataValue, MultiTransaction};
|
use cozo::{DataValue, MultiTransaction};
|
||||||
use panorama_core::state::node::ExtraData;
|
use itertools::Itertools;
|
||||||
|
use miette::IntoDiagnostic;
|
||||||
|
use panorama_core::{
|
||||||
|
state::node::{CreateOrUpdate, ExtraData},
|
||||||
|
NodeId,
|
||||||
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use utoipa::{OpenApi, ToSchema};
|
use utoipa::{OpenApi, ToSchema};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{error::AppResult, AppState};
|
use crate::{error::AppResult, AppState};
|
||||||
|
|
||||||
|
@ -89,70 +98,16 @@ pub struct UpdateData {
|
||||||
pub async fn update_node(
|
pub async fn update_node(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(node_id): Path<String>,
|
Path(node_id): Path<String>,
|
||||||
Json(update_data): Json<UpdateData>,
|
Json(opts): Json<UpdateData>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let node_id_data = DataValue::from(node_id.clone());
|
let node_id = NodeId(Uuid::from_str(&node_id).into_diagnostic()?);
|
||||||
|
let node_info = state
|
||||||
|
.create_or_update_node(CreateOrUpdate::Update { node_id }, opts.extra_data)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// TODO: Combine these into the same script
|
Ok(Json(json!({
|
||||||
|
"node_id": node_info.node_id.to_string(),
|
||||||
let tx = state.db.multi_transaction(true);
|
})))
|
||||||
|
|
||||||
if let Some(title) = update_data.title {
|
|
||||||
let title = DataValue::from(title);
|
|
||||||
|
|
||||||
tx.run_script(
|
|
||||||
"
|
|
||||||
# Always update the time
|
|
||||||
?[ id, title ] <- [[ $node_id, $title ]]
|
|
||||||
:update node { id, title }
|
|
||||||
",
|
|
||||||
btmap! {
|
|
||||||
"node_id".to_owned() => node_id_data.clone(),
|
|
||||||
"title".to_owned() => title,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(extra_data) = update_data.extra_data {
|
|
||||||
let result = get_rows_for_extra_keys(&tx, &extra_data)?;
|
|
||||||
|
|
||||||
for (key, (relation, field_name, ty)) in result.iter() {
|
|
||||||
let new_value = extra_data.get(key).unwrap();
|
|
||||||
|
|
||||||
// TODO: Make this more generic
|
|
||||||
let new_value = DataValue::from(new_value.as_str().unwrap());
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"
|
|
||||||
?[ node_id, {field_name} ] <- [[$node_id, $input_data]]
|
|
||||||
:update {relation} {{ node_id, {field_name} }}
|
|
||||||
"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = tx.run_script(
|
|
||||||
&query,
|
|
||||||
btmap! {
|
|
||||||
"node_id".to_owned() => node_id_data.clone(),
|
|
||||||
"input_data".to_owned() => new_value,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.run_script(
|
|
||||||
"
|
|
||||||
# Always update the time
|
|
||||||
?[ id, updated_at ] <- [[ $node_id, now() ]]
|
|
||||||
:update node { id, updated_at }
|
|
||||||
",
|
|
||||||
btmap! {
|
|
||||||
"node_id".to_owned() => node_id_data,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
|
|
||||||
Ok(Json(json!({})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -174,7 +129,10 @@ pub async fn create_node(
|
||||||
Json(opts): Json<CreateNodeOpts>,
|
Json(opts): Json<CreateNodeOpts>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let node_info = state
|
let node_info = state
|
||||||
.create_or_update_node(opts.ty, opts.extra_data)
|
.create_or_update_node(
|
||||||
|
CreateOrUpdate::Create { r#type: opts.ty },
|
||||||
|
opts.extra_data,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
|
@ -197,9 +155,13 @@ pub async fn search_nodes(
|
||||||
Query(query): Query<SearchQuery>,
|
Query(query): Query<SearchQuery>,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let search_result = state.search_nodes(query.query).await?;
|
let search_result = state.search_nodes(query.query).await?;
|
||||||
|
let search_result = search_result
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, value)| value["fields"].clone())
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"results": search_result
|
"results": search_result,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue