Add local document JSON state commands
This commit is contained in:
parent
b102e07204
commit
e039a4f81f
11 changed files with 153 additions and 10 deletions
|
|
@ -256,6 +256,8 @@ pub enum DbCommand {
|
|||
pub enum DocumentCommand {
|
||||
Create { name: String },
|
||||
Status { name: String },
|
||||
Set { name: String, state_json: String },
|
||||
Get { name: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -471,6 +473,10 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
Command::Document { command } => match command {
|
||||
DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name },
|
||||
DocumentCommand::Status { name } => ControlRequest::DocumentStatus { name },
|
||||
DocumentCommand::Set { name, state_json } => {
|
||||
ControlRequest::DocumentSet { name, state_json }
|
||||
}
|
||||
DocumentCommand::Get { name } => ControlRequest::DocumentGet { name },
|
||||
},
|
||||
Command::Ssh { command } => match command {
|
||||
SshCommand::Proxy { node } => ControlRequest::ModuleStub {
|
||||
|
|
@ -904,6 +910,14 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
println!("sync_status: {}", document.sync_status);
|
||||
println!("state_bytes: {}", document.state_bytes);
|
||||
}
|
||||
ControlResponse::DocumentSet { state } => {
|
||||
println!("updated document: {}", state.document.name);
|
||||
println!("state_bytes: {}", state.document.state_bytes);
|
||||
println!("updated_at_ms: {}", state.updated_at.0);
|
||||
}
|
||||
ControlResponse::DocumentGet { state } => {
|
||||
println!("{}", state.state_json);
|
||||
}
|
||||
ControlResponse::PubsubPublished { message } => {
|
||||
println!("published: {}", message.topic);
|
||||
println!("published_at_ms: {}", message.published_at.0);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use geth_auth::{AuthExplanation, AuthOp};
|
||||
use geth_db::DbResource;
|
||||
use geth_document::DocumentResource;
|
||||
use geth_document::{DocumentResource, DocumentState};
|
||||
use geth_keychain::KeychainOp;
|
||||
use geth_kv::{KvEntry, KvResource};
|
||||
use geth_pubsub::PubsubMessage;
|
||||
|
|
@ -138,6 +138,13 @@ pub enum ControlRequest {
|
|||
DocumentStatus {
|
||||
name: String,
|
||||
},
|
||||
DocumentSet {
|
||||
name: String,
|
||||
state_json: String,
|
||||
},
|
||||
DocumentGet {
|
||||
name: String,
|
||||
},
|
||||
PubsubPub {
|
||||
topic: String,
|
||||
message: String,
|
||||
|
|
@ -263,6 +270,12 @@ pub enum ControlResponse {
|
|||
DocumentStatus {
|
||||
document: DocumentResource,
|
||||
},
|
||||
DocumentSet {
|
||||
state: DocumentState,
|
||||
},
|
||||
DocumentGet {
|
||||
state: DocumentState,
|
||||
},
|
||||
PubsubPublished {
|
||||
message: PubsubMessage,
|
||||
},
|
||||
|
|
@ -384,5 +397,14 @@ mod tests {
|
|||
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
||||
response
|
||||
);
|
||||
|
||||
let request = ControlRequest::DocumentSet {
|
||||
name: "notes".to_owned(),
|
||||
state_json: r#"{"title":"notes"}"#.to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||
request
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use geth_types::{DocumentId, ResourceId};
|
||||
use geth_types::{DocumentId, ResourceId, UnixMillis};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -10,10 +10,19 @@ pub struct DocumentResource {
|
|||
pub state_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DocumentState {
|
||||
pub document: DocumentResource,
|
||||
pub state_json: String,
|
||||
pub updated_at: UnixMillis,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DocumentError {
|
||||
#[error("invalid document name: {0}")]
|
||||
InvalidName(String),
|
||||
#[error("invalid document JSON state: {0}")]
|
||||
InvalidState(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
pub fn validate_document_name(name: &str) -> Result<(), DocumentError> {
|
||||
|
|
@ -27,6 +36,11 @@ pub fn validate_document_name(name: &str) -> Result<(), DocumentError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_document_state(state_json: &str) -> Result<String, DocumentError> {
|
||||
let value: serde_json::Value = serde_json::from_str(state_json)?;
|
||||
Ok(serde_json::to_string(&value)?)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn automerge_roadmap() -> &'static str {
|
||||
"future documents use Automerge sync over Iroh with resource-local authorization"
|
||||
|
|
@ -45,4 +59,13 @@ mod tests {
|
|||
assert!(validate_document_name("notes/main").is_err());
|
||||
assert!(validate_document_name("notes main").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_state_is_validated_and_normalized_json() {
|
||||
assert_eq!(
|
||||
normalize_document_state(r#"{ "title": "notes", "done": false }"#).expect("normalize"),
|
||||
r#"{"done":false,"title":"notes"}"#
|
||||
);
|
||||
assert!(normalize_document_state("{").is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use geth_control::{
|
|||
};
|
||||
use geth_crypto::AgentKey;
|
||||
use geth_db::DbResource;
|
||||
use geth_document::DocumentResource;
|
||||
use geth_document::{DocumentResource, DocumentState};
|
||||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||
use geth_kv::{KvEntry, KvResource};
|
||||
|
|
@ -72,6 +72,8 @@ pub enum NodeError {
|
|||
InvalidDocumentName(String),
|
||||
#[error("document not found: {0}")]
|
||||
DocumentNotFound(String),
|
||||
#[error("document error: {0}")]
|
||||
Document(#[from] geth_document::DocumentError),
|
||||
#[error("resource not found: {0}")]
|
||||
ResourceNotFound(String),
|
||||
#[error("secrets error: {0}")]
|
||||
|
|
@ -826,6 +828,29 @@ pub fn handle_request(
|
|||
document: document_resource_from_stored(&stored),
|
||||
})
|
||||
}
|
||||
ControlRequest::DocumentSet { name, state_json } => {
|
||||
geth_document::validate_document_name(&name)
|
||||
.map_err(|_| NodeError::InvalidDocumentName(name.clone()))?;
|
||||
let mut stored = store
|
||||
.get_document_resource_by_name(&name)?
|
||||
.ok_or_else(|| NodeError::DocumentNotFound(name.clone()))?;
|
||||
stored.state_json = geth_document::normalize_document_state(&state_json)?;
|
||||
stored.updated_at_ms = geth_store::now_ms();
|
||||
store.insert_document_resource(&stored)?;
|
||||
Ok(ControlResponse::DocumentSet {
|
||||
state: document_state_from_stored(&stored),
|
||||
})
|
||||
}
|
||||
ControlRequest::DocumentGet { name } => {
|
||||
geth_document::validate_document_name(&name)
|
||||
.map_err(|_| NodeError::InvalidDocumentName(name.clone()))?;
|
||||
let stored = store
|
||||
.get_document_resource_by_name(&name)?
|
||||
.ok_or_else(|| NodeError::DocumentNotFound(name.clone()))?;
|
||||
Ok(ControlResponse::DocumentGet {
|
||||
state: document_state_from_stored(&stored),
|
||||
})
|
||||
}
|
||||
ControlRequest::PubsubPub { topic, message } => {
|
||||
geth_pubsub::validate_topic(&topic)?;
|
||||
geth_pubsub::validate_message(&message)?;
|
||||
|
|
@ -938,6 +963,14 @@ fn document_resource_from_stored(stored: &StoredDocumentResource) -> DocumentRes
|
|||
}
|
||||
}
|
||||
|
||||
fn document_state_from_stored(stored: &StoredDocumentResource) -> DocumentState {
|
||||
DocumentState {
|
||||
document: document_resource_from_stored(stored),
|
||||
state_json: stored.state_json.clone(),
|
||||
updated_at: UnixMillis(stored.updated_at_ms),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_resource_exists(store: &Store, resource_id: &str) -> Result<(), NodeError> {
|
||||
if store
|
||||
.list_resources()?
|
||||
|
|
|
|||
|
|
@ -589,6 +589,50 @@ fn document_create_and_status_use_local_store() {
|
|||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DocumentSet {
|
||||
name: "notes".to_owned(),
|
||||
state_json: r#"{ "title": "notes", "items": [1, 2] }"#.to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("document set");
|
||||
match response {
|
||||
geth_control::ControlResponse::DocumentSet { state } => {
|
||||
assert_eq!(state.document.name, "notes");
|
||||
assert_eq!(state.state_json, r#"{"items":[1,2],"title":"notes"}"#);
|
||||
assert!(state.document.state_bytes > 2);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let reopened = geth_node::open_node(&paths).expect("reopen node");
|
||||
let response = geth_node::handle_request(
|
||||
&reopened,
|
||||
geth_control::ControlRequest::DocumentGet {
|
||||
name: "notes".to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("document get");
|
||||
match response {
|
||||
geth_control::ControlResponse::DocumentGet { state } => {
|
||||
assert_eq!(state.state_json, r#"{"items":[1,2],"title":"notes"}"#);
|
||||
assert!(state.updated_at.0 > 0);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DocumentSet {
|
||||
name: "notes".to_owned(),
|
||||
state_json: "{".to_owned(),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
|
|
|
|||
Loading…
Reference in a new issue