diff --git a/docs/HTTP_API.md b/docs/HTTP_API.md index dbaa9ea7196e4d5dd21f3c00567275c4033a138a..edb5fda84d348310814c5dd399e80e5814936fcc 100644 --- a/docs/HTTP_API.md +++ b/docs/HTTP_API.md @@ -196,6 +196,47 @@ Mark an item as deleted: * Update `dateModified` (server's time is taken) +### POST /v2/$owner_key/insert_tree +```json5 +{ + "databaseKey": "2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99", + "payload": { /* item definition (see below) */ } +} +``` +Insert a tree with edges (of arbitrary depth) in one batch. + +Each item should either be an object with only `uid` and `_edges` fields: +```json5 +{ + "uid": 123456789 /* uid of the item to create edge with */, + "_edges": [ /* see below edges definition*/ ] +} +``` +Or the full item to be created, in which case `uid` is optional, +but all standard mandatory item fields need to be present: +```json5 +{ + "_type": "SomeItemType", + "_edges": [ /* see below edges definition*/ ], + /* other item properties here */ +} +``` + +Each edge in the array above is required to have the following form: +```json5 +{ + "_type": "SomeEdgeType", + "_target": { /* item of identical structure to the above */ } + /* optional edge properties here */ +} +``` + +As always, inserting edges will result in updating timestamps for `_source` items +(even if they are referenced by `uid` only). + +The method will return the `uid` of the created root item, e.g. `123456789`. + + ### POST /v2/$owner_key/search_by_fields/ ```json { @@ -204,8 +245,10 @@ Mark an item as deleted: } ``` Search items by their fields. -Field `_dateServerModifiedAfter` is not treated in the standard way, and instead, it filters -items by their `_dateServerModified` field using the `>` operator. + +Ephemeral underscore field `_dateServerModifiedAfter`, if specified, +is treated specially. It will filter out those items that have +`_dateServerModified` higher (`>`) than the specified value. The endpoint will return an array of all items with exactly the same properties. diff --git a/src/api_model.rs b/src/api_model.rs index eefcd15e8b51b7a048f2b17934cab9678d315074..29ec34fd19bb74f762d40d06fec1509af738af06 100644 --- a/src/api_model.rs +++ b/src/api_model.rs @@ -49,6 +49,22 @@ pub struct BulkAction { pub delete_edges: Vec<DeleteEdge>, } +#[derive(Serialize, Deserialize, Debug)] +pub struct InsertTreeItem { + #[serde(default)] + pub _edges: Vec<InsertTreeEdge>, + #[serde(flatten)] + pub fields: HashMap<String, Value>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct InsertTreeEdge { + pub _type: String, + pub _target: InsertTreeItem, + #[serde(flatten)] + pub fields: HashMap<String, Value>, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SearchByFields { diff --git a/src/internal_api.rs b/src/internal_api.rs index f830e65728ba65d6b9834f70ade3b27b3e04fb16..110a4629d1a09d28d563749e7c3a5c576e1453e2 100644 --- a/src/internal_api.rs +++ b/src/internal_api.rs @@ -1,6 +1,6 @@ use crate::api_model::BulkAction; -use crate::api_model::CreateItem; use crate::api_model::DeleteEdge; +use crate::api_model::InsertTreeItem; use crate::api_model::SearchByFields; use crate::error::Error; use crate::error::Result; @@ -250,12 +250,25 @@ pub fn bulk_action_tx(tx: &Transaction, bulk_action: BulkAction) -> Result<()> { Ok(()) } -pub fn create_item(conn: &mut Connection, create_action: CreateItem) -> Result<i64> { - debug!("Creating item {:?}", create_action); - let tx = conn.transaction()?; - let result = create_item_tx(&tx, create_action.fields)?; - tx.commit()?; - Ok(result) +pub fn insert_tree(tx: &Transaction, item: InsertTreeItem) -> Result<i64> { + let source_uid: i64 = if item.fields.len() > 1 { + create_item_tx(tx, item.fields)? + } else if let Some(uid) = item.fields.get("uid").map(|v| v.as_i64()).flatten() { + if !item._edges.is_empty() { + update_item_tx(tx, uid, HashMap::new())?; + } + uid + } else { + return Err(Error { + code: StatusCode::BAD_REQUEST, + msg: format!("Cannot create item: {:?}", item), + }); + }; + for edge in item._edges { + let target_item = insert_tree(tx, edge._target)?; + create_edge(tx, &edge._type, source_uid, target_item, edge.fields)?; + } + Ok(source_uid) } pub fn search_by_fields(tx: &Transaction, query: SearchByFields) -> Result<Vec<Value>> { diff --git a/src/warp_api.rs b/src/warp_api.rs index f8642bd078f9227a877fa116ce90cbb2e5da6add..cab5ebea4d2d81443f0eae1f98b24c782cc0856b 100644 --- a/src/warp_api.rs +++ b/src/warp_api.rs @@ -1,6 +1,7 @@ use crate::api_model::BulkAction; use crate::api_model::CreateItem; use crate::api_model::GetFile; +use crate::api_model::InsertTreeItem; use crate::api_model::PayloadWrapper; use crate::api_model::RunDownloader; use crate::api_model::RunImporter; @@ -126,7 +127,18 @@ pub async fn run_server(cli_options: &CLIOptions) { }); let init_db = initialized_databases_arc.clone(); - let search = items_api + let insert_tree = items_api + .and(warp::path!(String / "insert_tree")) + .and(warp::path::end()) + .and(warp::body::json()) + .map(move |owner: String, body: PayloadWrapper<InsertTreeItem>| { + let result = warp_endpoints::insert_tree(owner, init_db.deref(), body); + let result = result.map(|result| warp::reply::json(&result)); + respond_with_result(result) + }); + + let init_db = initialized_databases_arc.clone(); + let search_by_fields = items_api .and(warp::path!(String / "search_by_fields")) .and(warp::path::end()) .and(warp::body::json()) @@ -250,7 +262,8 @@ pub async fn run_server(cli_options: &CLIOptions) { .or(bulk_action.with(&headers)) .or(update_item.with(&headers)) .or(delete_item.with(&headers)) - .or(search.with(&headers)) + .or(insert_tree.with(&headers)) + .or(search_by_fields.with(&headers)) .or(get_items_with_edges.with(&headers)) .or(run_downloader.with(&headers)) .or(run_importer.with(&headers)) diff --git a/src/warp_endpoints.rs b/src/warp_endpoints.rs index f79251b40adfb5b9e12b2a01d0b83f761500951c..b07ff183ea5bb2d3e4d9ec47f3b8cde9c8a0c025 100644 --- a/src/warp_endpoints.rs +++ b/src/warp_endpoints.rs @@ -1,6 +1,7 @@ use crate::api_model::BulkAction; use crate::api_model::CreateItem; use crate::api_model::GetFile; +use crate::api_model::InsertTreeItem; use crate::api_model::PayloadWrapper; use crate::api_model::RunDownloader; use crate::api_model::RunImporter; @@ -94,6 +95,15 @@ pub fn delete_item( }) } +pub fn insert_tree( + owner: String, + init_db: &RwLock<HashSet<String>>, + body: PayloadWrapper<InsertTreeItem>, +) -> Result<i64> { + let mut conn: Connection = check_owner_and_initialize_db(&owner, &init_db, &body.database_key)?; + in_transaction(&mut conn, |tx| internal_api::insert_tree(&tx, body.payload)) +} + pub fn search_by_fields( owner: String, init_db: &RwLock<HashSet<String>>,