add updates

This commit is contained in:
Conrad Ludgate 2023-07-12 10:03:04 +01:00
parent 9347eedbe3
commit 214e3c337c
5 changed files with 181 additions and 36 deletions

22
Cargo.lock generated
View File

@ -75,6 +75,12 @@ dependencies = [
"password-hash",
]
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-trait"
version = "0.1.58"
@ -270,6 +276,7 @@ dependencies = [
"async-trait",
"atuin-client",
"atuin-common",
"chrono",
"clap",
"eyre",
"fs-err",
@ -278,6 +285,8 @@ dependencies = [
"serde_json",
"tantivy",
"tokio",
"type-safe-id",
"uuid",
]
[[package]]
@ -3507,6 +3516,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "type-safe-id"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1211a53644c6a5ae26c45bdd520d1533b07dab50dc6af1a15063772f9061ad"
dependencies = [
"arrayvec",
"rand 0.8.5",
"serde",
"thiserror",
"uuid",
]
[[package]]
name = "typed-builder"
version = "0.14.0"

View File

@ -11,6 +11,7 @@ atuin-client = { path = "../atuin-client", version = "15.0.0" }
eyre = { workspace = true }
async-trait = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
fs-err = { workspace = true }
rmp = { version = "0.8.11" }
@ -18,3 +19,5 @@ serde = { workspace = true }
serde_json = { workspace = true }
tantivy = "0.20.2"
tokio = { workspace = true }
type-safe-id = { version = "0.2.1", features = ["serde"] }
uuid = { workspace = true }

View File

@ -8,13 +8,23 @@ use atuin_client::{
settings::Settings,
};
use atuin_common::record::HostId;
use chrono::{Local, TimeZone};
use clap::Parser;
use eyre::{bail, Context, Result};
use record::TodoStore;
use record::{TodoRecord, TodoStore};
use type_safe_id::{StaticType, TypeSafeId};
use uuid::Uuid;
mod record;
mod search;
#[derive(Default, Copy, Clone, PartialEq, Eq)]
pub struct Todo;
impl StaticType for Todo {
const TYPE: &'static str = "todo";
}
pub type TodoId = TypeSafeId<Todo>;
#[derive(clap::Parser)]
#[command(author = "Conrad Ludgate <conradludgate@gmail.com>")]
enum Cmd {
@ -30,6 +40,25 @@ enum Cmd {
/// todo text
text: String,
},
Update {
#[arg(long)]
id: TodoId,
/// The state this todo item is in. eg "todo", "in progress", etc
#[arg(short = 's', long = "state")]
state: Option<String>,
/// Whether to append the new tags or overwrite the tags
#[arg(short, long)]
append: bool,
/// tags for this todo item
#[arg(short = 't', long = "tag")]
tags: Vec<String>,
/// todo text
text: Option<String>,
},
Search {
#[arg(long, default_value = "20")]
limit: usize,
@ -37,6 +66,10 @@ enum Cmd {
/// search query
query: String,
},
View {
#[arg(long)]
id: TodoId,
},
}
#[tokio::main]
@ -66,15 +99,74 @@ async fn main() -> Result<()> {
match cmd {
Cmd::Push { state, tags, text } => {
todo_store
.create_item(&mut store, &encryption_key, state, text, tags)
let record = TodoRecord {
tags,
state,
text,
updates: TodoId::from_uuid(Uuid::nil()),
};
let record = todo_store
.create_item(&mut store, &encryption_key, record)
.await?;
println!("created {}", TodoId::from_uuid(record.id.0));
}
Cmd::Update {
id,
state,
append,
mut tags,
text,
} => {
let mut record = todo_store.get(&mut store, &encryption_key, id).await?.data;
record.updates = id;
if append {
record.tags.append(&mut tags)
} else if !tags.is_empty() {
record.tags = tags;
}
if let Some(state) = state {
record.state = state;
}
if let Some(text) = text {
record.text = text;
}
let record = todo_store
.create_item(&mut store, &encryption_key, record)
.await?;
println!("updated from {} to {}", id, TodoId::from_uuid(record.id.0));
}
Cmd::Search { limit, query } => {
let ids = todo_store.search(&query, limit).await?;
for id in ids {
let record = todo_store.get(&mut store, &encryption_key, id).await?;
println!("{}", serde_json::to_string(&record).unwrap());
println!("{id} - [{}] {}", record.data.state, record.data.text);
if !record.data.tags.is_empty() {
print!("\t");
for tag in record.data.tags {
print!("#{tag} ")
}
println!()
}
}
}
Cmd::View { mut id } => {
while !id.uuid().is_nil() {
let record = todo_store.get(&mut store, &encryption_key, id).await?;
id = record.data.updates;
let ts = Local
.timestamp_nanos(record.timestamp as i64)
.format("%Y %B %-d %R");
println!("{ts} - [{}] {}", record.data.state, record.data.text);
if !record.data.tags.is_empty() {
print!("\t");
for tag in record.data.tags {
print!("#{tag} ")
}
println!()
}
}
}
}

View File

@ -21,19 +21,24 @@
//! state,
//! text,
//! [tag],
//! updates,
//! ]
//! ```
use atuin_common::record::{DecryptedData, Record, RecordId};
use eyre::{bail, ensure, eyre, Result};
use atuin_common::record::{DecryptedData, EncryptedData, Record, RecordId};
use eyre::{bail, ensure, eyre, Context, ContextCompat, Result};
use atuin_client::record::encryption::paseto_v4::PASETO_V4;
use atuin_client::record::store::Store;
use atuin_client::settings::Settings;
use serde::Serialize;
use tantivy::{collector::TopDocs, query::QueryParser, Index};
use uuid::Uuid;
use crate::search::{self, TodoSchema};
use crate::{
search::{self, TodoSchema},
TodoId,
};
const TODO_VERSION: &str = "v0";
const TODO_TAG: &str = "todo";
@ -43,6 +48,7 @@ pub struct TodoRecord {
pub state: String,
pub text: String,
pub tags: Vec<String>,
pub updates: TodoId,
}
impl TodoRecord {
@ -52,7 +58,7 @@ impl TodoRecord {
let mut output = vec![];
// INFO: ensure this is updated when adding new fields
encode::write_array_len(&mut output, 3)?;
encode::write_array_len(&mut output, 4)?;
encode::write_str(&mut output, &self.state)?;
encode::write_str(&mut output, &self.text)?;
@ -62,6 +68,8 @@ impl TodoRecord {
encode::write_str(&mut output, tag)?;
}
encode::write_bin(&mut output, self.updates.uuid().as_bytes())?;
Ok(DecryptedData(output))
}
@ -74,32 +82,39 @@ impl TodoRecord {
match version {
TODO_VERSION => {
let mut bytes = decode::Bytes::new(&data.0);
// let mut rd = decode::Bytes::new(&data.0);
let mut data = &*data.0;
let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
ensure!(nfields == 3, "too many entries in v0 todo record");
let nfields = decode::read_array_len(&mut data)?;
ensure!(
nfields == 4,
"incorrect number of entries in v0 todo record"
);
let bytes = bytes.remaining_slice();
let (state, data) = decode::read_str_from_slice(data).map_err(error_report)?;
let (text, mut data) = decode::read_str_from_slice(data).map_err(error_report)?;
let (state, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
let (text, mut bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
let ntags = decode::read_array_len(&mut bytes).map_err(error_report)?;
let ntags = decode::read_array_len(&mut data)?;
let mut tags = Vec::with_capacity(ntags as usize);
for _ in 0..ntags {
let (value, b) = decode::read_str_from_slice(bytes).map_err(error_report)?;
bytes = b;
let (value, b) = decode::read_str_from_slice(data).map_err(error_report)?;
data = b;
tags.push(value.to_owned())
}
if !bytes.is_empty() {
bail!("trailing bytes in encoded todo record. malformed")
}
let updates_len = decode::read_bin_len(&mut data)?;
ensure!(updates_len == 16, "incorrect UUID encoding in todo record");
let updates: [u8; 16] = data
.try_into()
.context("incorrect UUID encoding in todo record")?;
let updates = TodoId::from_uuid(Uuid::from_bytes(updates));
Ok(TodoRecord {
state: state.to_owned(),
text: text.to_owned(),
tags,
updates,
})
}
_ => {
@ -132,14 +147,10 @@ impl TodoStore {
&self,
store: &mut (impl Store + Send + Sync),
encryption_key: &[u8; 32],
state: String,
text: String,
tags: Vec<String>,
) -> Result<()> {
record: TodoRecord,
) -> Result<Record<EncryptedData>> {
let host_id = Settings::host_id().expect("failed to get host_id");
let todo = TodoRecord { state, text, tags };
let parent = store.tail(host_id, TODO_TAG).await?.map(|entry| entry.id);
let record = Record::builder()
@ -147,7 +158,7 @@ impl TodoStore {
.version(TODO_VERSION.to_string())
.tag(TODO_TAG.to_owned())
.parent(parent)
.data(todo)
.data(record)
.build();
let mut writer = self.index.writer(3_000_000)?;
@ -162,16 +173,16 @@ impl TodoStore {
let record = record.encrypt::<PASETO_V4>(encryption_key);
store.push(&record).await?;
Ok(())
Ok(record)
}
pub async fn get(
&self,
store: &mut (impl Store + Send + Sync),
encryption_key: &[u8; 32],
id: RecordId,
id: TodoId,
) -> Result<Record<TodoRecord>> {
let record = store.get(id).await?;
let record = store.get(RecordId(id.uuid())).await?;
match &*record.version {
"v0" => {
let record = record.decrypt::<PASETO_V4>(encryption_key)?;
@ -182,7 +193,7 @@ impl TodoStore {
}
}
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<RecordId>> {
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<TodoId>> {
let query_parser = QueryParser::new(
self.index.schema(),
vec![self.schema.text, self.schema.tag],
@ -197,7 +208,12 @@ impl TodoStore {
for (_, doc) in docs {
let doc = searcher.doc(doc)?;
let id = doc.get_first(self.schema.id);
output.push(RecordId(id.unwrap().as_text().unwrap().parse().unwrap()))
output.push(
id.context("missing id")?
.as_text()
.context("invalid id")?
.parse()?,
)
}
Ok(output)
@ -206,6 +222,10 @@ impl TodoStore {
#[cfg(test)]
mod tests {
use uuid::Uuid;
use crate::TodoId;
use super::{TodoRecord, TODO_VERSION};
#[test]
@ -214,6 +234,7 @@ mod tests {
state: "todo".to_owned(),
text: "implement todo".to_owned(),
tags: vec!["atuin".to_owned(), "rust".to_owned()],
updates: TodoId::from_uuid(Uuid::nil()),
};
let encoded = kv.serialize().unwrap();

View File

@ -4,10 +4,10 @@ use tantivy::{
directory::MmapDirectory,
doc,
schema::{Field, Schema, STORED, STRING, TEXT},
DateTime, Index, IndexWriter,
DateTime, Index, IndexWriter, Term,
};
use crate::record::TodoRecord;
use crate::{record::TodoRecord, TodoId};
pub fn schema() -> (TodoSchema, Schema) {
let mut schema_builder = Schema::builder();
@ -47,7 +47,7 @@ pub fn write_record(
) -> Result<()> {
let timestamp = DateTime::from_timestamp_nanos(record.timestamp as i64);
let mut doc = doc!(
schema.id => record.id.0.to_string(),
schema.id => TodoId::from_uuid(record.id.0).to_string(),
schema.text => record.data.text.clone(),
schema.timestamp => timestamp,
);
@ -56,5 +56,12 @@ pub fn write_record(
}
writer.add_document(doc)?;
if !record.data.updates.uuid().is_nil() {
writer.delete_term(Term::from_field_text(
schema.id,
&record.data.updates.to_string(),
));
}
Ok(())
}