Commit 76096c0f authored by Vasili Novikov's avatar Vasili Novikov
Browse files

Merge branch 'docs-edges-endpoints' into 'dev'

Add basic edges endpoints

See merge request memri/pod!193
parents d7b681d9 9b5cc3fe
Showing with 313 additions and 65 deletions
+313 -65
......@@ -391,6 +391,25 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "field_count"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284d5f85dd574cf01094bca24aefa69a43539dbfc72b1326f038d540b2daadc7"
dependencies = [
"field_count_derive",
]
[[package]]
name = "field_count_derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1320970ff3b1c1cacc6a38e8cdb1aced955f29627697cd992c5ded82eb646a8"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "fnv"
version = "1.0.7"
......@@ -984,6 +1003,8 @@ dependencies = [
"chrono",
"criterion",
"env_logger",
"field_count",
"field_count_derive",
"hex",
"lazy_static",
"libc",
......
......@@ -16,6 +16,8 @@ bytes = "1.0.1"
chacha20poly1305 = "0.7.1"
chrono = { version = "0.4.19", features = ["serde"] }
env_logger = "0.8.3"
field_count = "0.1.1"
field_count_derive = "0.1.1"
hex = "0.4.2"
lazy_static = "1.4.0"
libc = "0.2.87"
......
......@@ -105,7 +105,7 @@ Get version of the Pod: the git commit and cargo version that it was built from.
```json
{
"auth": $auth_json,
"payload": $id
"payload": "$id"
}
```
Get a single item by its `id`.
......@@ -145,7 +145,7 @@ Returns `id` of the created item if the operation is successful.
```json
{
"auth": $auth_json,
"payload": { "id": $id, ... }
"payload": { "id": "$id", ... }
}
```
Update a single item.
......@@ -161,11 +161,74 @@ Update a single item.
Returns an empty object if the operation is successful.
### POST /v3/$owner_key/get_edges
```json5
{
"auth": $auth_json,
"payload": {
"item": "$id",
"direction": "OUTGOING", // Either OUTGOING (default) or INCOMING edges
"expandItems": true // Whether to expand the target/source items for each edge
}
}
```
Get all edges for a single item.
Example output:
```json5
[
{
"name": "friend",
item: {
"id": "00000000",
// all other fields if "expandItems" is requested
}
},
{
"name": "friend",
item: {
"id": ".........."
}
},
...
]
```
Returns an array empty array if either the element does not exist or if it has no outgoing edges.
⚠ WARNING: this endpoint is unstable, and it might be deprecated and removed in next releases of Pod.️
### POST /v3/$owner_key/create_edge
```json5
{
"auth": $auth_json,
"payload": {
"_source": "$source_id", /* Source item id */
"_target": "$target_id", /* Target item id */
"_name": "$edge_name" /* Text name. For example: "entry" (in a list), "friend" (for a Person), etc */
// Edge properties will be supported in the future
}
}
```
Create a single edge from an already existing item to another already existing item.
(Reminder: edges are always directed.)
An error will be returned if the edge already exists.
An error will be returned if source item or target item do not exist.
Returns `id` of the created edge if the operation is successful
(for now, nothing is possible to do with the id, but in the future there will be more options).
Returns an error if such `id` already exist in the DB.
Returns an error if the new item doesn't conform to the Schema.
Returns `id` of the created item if the operation is successful.
### POST /v3/$owner_key/delete_item
```json
{
"auth": $auth_json,
"payload": $id
"payload": "$id"
}
```
Mark an item as deleted:
......@@ -202,12 +265,21 @@ In the future, any item properties will be available.
"auth": $auth_json,
"payload": {
"createItems": [
{ "id": "something-12345", "type": "Person", ... }, ...
{ "id": "something-12345", "type": "Person", ... },
{}, // same structure as create_item endpoint above
...
],
"updateItems": [
{ "id": "something-67899", ... }, ...
{ "id": "something-67899", ... },
{}, // same structure as update_item endpoint above
...
],
"deleteItems": [ $id, $id, $id, ...],
"deleteItems": [ "$id", "$id", "$id", ...],
"createEdges": [
{}, // same structure as create_edge endpoint above
{},
...
]
}
}
```
......@@ -267,7 +339,7 @@ The properties `nonce` and `key` will be updated for this item.
{
"auth": $auth_json,
"payload": {
"sha256": $sha256
"sha256": "$sha256"
}
}
```
......
......@@ -78,6 +78,30 @@ pub struct Bulk {
pub delete_items: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CreateEdge {
#[serde(rename = "_source")]
pub source: String,
#[serde(rename = "_target")]
pub target: String,
#[serde(rename = "_name")]
pub name: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetEdges {
pub item: String,
pub direction: EdgeDirection,
pub expand_items: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum EdgeDirection {
Outgoing,
Incoming,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Search {
......
......@@ -4,6 +4,7 @@ use crate::error::ErrorContext;
use crate::error::Result;
use crate::schema::Schema;
use crate::schema::SchemaPropertyType;
use field_count::FieldCount;
use log::debug;
use rusqlite::params;
use rusqlite::types::ToSqlOutput;
......@@ -16,6 +17,7 @@ use warp::http::StatusCode;
pub type Rowid = i64;
pub type DbTime = i64;
#[derive(FieldCount)]
pub struct ItemBase {
pub rowid: Rowid,
pub id: String,
......@@ -59,6 +61,21 @@ pub fn insert_item_base(
.context_str("Failed to execute insert_item with parameters")
}
pub fn get_item_base(tx: &Tx, rowid: Rowid) -> Result<Option<ItemBase>> {
let database_search = DatabaseSearch {
rowid: Some(rowid),
id: None,
_type: None,
date_server_modified_gte: None,
date_server_modified_lt: None,
deleted: None,
sort_order: SortOrder::Asc,
_limit: 1,
};
let item = search_items(tx, &database_search)?.into_iter().next();
Ok(item)
}
pub fn get_item_rowid(tx: &Tx, id: &str) -> Result<Option<Rowid>> {
let mut stmt = tx.prepare_cached("SELECT rowid FROM items WHERE id = ?;")?;
let mut rows = stmt.query_map(params![id], |row| row.get(0))?;
......@@ -310,22 +327,46 @@ pub fn delete_property(tx: &Tx, item: Rowid, name: &str) -> Result<()> {
Ok(())
}
/// Low-level function to insert an edge.
/// No Schema/type checks are done. Use other functions around instead.
#[allow(dead_code)]
fn insert_edge_unchecked(
pub fn insert_edge(
tx: &Tx,
self_rowid: Rowid,
source: Rowid,
name: &str,
target: Rowid,
id: &str,
date: DbTime,
) -> Result<Rowid> {
let item = insert_item_base(tx, id, name, date, date, date, false)?;
let mut stmt =
tx.prepare_cached("INSERT INTO edges(self, source, name, target) VALUES(?, ?, ?, ?);")?;
stmt.execute(params![item, source, name, target])?;
Ok(item)
stmt.execute(params![self_rowid, source, name, target])?;
Ok(tx.last_insert_rowid())
}
pub struct HalfEdge {
pub name: String,
pub item: Rowid,
}
pub fn get_outgoing_edges(tx: &Tx, source: Rowid) -> Result<Vec<HalfEdge>> {
let mut stmt = tx.prepare_cached("SELECT target, name FROM edges WHERE source = ?;")?;
let mut rows = stmt.query(params![source])?;
let mut result = Vec::new();
while let Some(row) = rows.next()? {
let item = row.get(0)?;
let name = row.get(1)?;
result.push(HalfEdge { name, item })
}
Ok(result)
}
pub fn get_incoming_edges(tx: &Tx, target: Rowid) -> Result<Vec<HalfEdge>> {
let mut stmt = tx.prepare_cached("SELECT source, name FROM edges WHERE target = ?;")?;
let mut rows = stmt.query(params![target])?;
let mut result = Vec::new();
while let Some(row) = rows.next()? {
let item = row.get(0)?;
let name = row.get(1)?;
result.push(HalfEdge { name, item })
}
Ok(result)
}
pub fn get_schema(tx: &Tx) -> Result<Schema> {
......@@ -452,7 +493,8 @@ mod tests {
let source = insert_item_base(&tx, &random_id(), "Person", date, date, date, false)?;
let target = insert_item_base(&tx, &random_id(), "Person", date, date, date, false)?;
assert_eq!(target - source, 1);
let edge = insert_edge_unchecked(&tx, source, "friend", target, &random_id(), date)?;
let item = insert_item_base(&tx, &random_id(), "Edge", date, date, date, false)?;
let edge = insert_edge(&tx, item, source, "friend", target)?;
assert_eq!(edge - target, 1);
Ok(())
}
......
use crate::api_model::Bulk;
use crate::api_model::CreateEdge;
use crate::api_model::CreateItem;
use crate::api_model::EdgeDirection;
use crate::api_model::GetEdges;
use crate::api_model::Search;
use crate::api_model::SortOrder;
use crate::command_line_interface;
......@@ -23,7 +26,7 @@ use chrono::Utc;
use log::info;
use log::warn;
use rand::Rng;
use rusqlite::Transaction;
use rusqlite::Transaction as Tx;
use serde_json::Map;
use serde_json::Value;
use std::collections::HashMap;
......@@ -36,11 +39,7 @@ pub fn get_project_version() -> String {
/// Get all properties that the item has, ignoring those
/// that exist in the DB but are not defined in the Schema
pub fn get_item_properties(
tx: &Transaction,
rowid: i64,
schema: &Schema,
) -> Result<Map<String, Value>> {
pub fn get_item_properties(tx: &Tx, rowid: i64, schema: &Schema) -> Result<Map<String, Value>> {
let mut json = serde_json::Map::new();
for IntegersNameValue { name, value } in database_api::get_integers_records_for_item(tx, rowid)?
......@@ -98,32 +97,18 @@ pub fn get_item_properties(
Ok(json)
}
pub fn get_item_from_rowid(tx: &Transaction, schema: &Schema, rowid: Rowid) -> Result<Value> {
let database_search = DatabaseSearch {
rowid: Some(rowid),
id: None,
_type: None,
date_server_modified_gte: None,
date_server_modified_lt: None,
deleted: None,
sort_order: SortOrder::Asc,
_limit: 1,
};
let item = database_api::search_items(tx, &database_search)?;
let item = if let Some(item) = item.into_iter().next() {
item
} else {
return Err(Error {
code: StatusCode::INTERNAL_SERVER_ERROR,
msg: format!("Item rowid {} not found right after inserting", rowid),
});
};
pub fn get_item_from_rowid(tx: &Tx, schema: &Schema, rowid: Rowid) -> Result<Value> {
let item = database_api::get_item_base(tx, rowid)?;
let item = item.ok_or_else(|| Error {
code: StatusCode::INTERNAL_SERVER_ERROR,
msg: format!("Item rowid {} not found right after inserting", rowid),
})?;
let mut props = get_item_properties(tx, rowid, schema)?;
add_item_base_properties(&mut props, item);
Ok(Value::Object(props))
}
pub fn get_item_tx(tx: &Transaction, schema: &Schema, id: &str) -> Result<Vec<Value>> {
pub fn get_item_tx(tx: &Tx, schema: &Schema, id: &str) -> Result<Vec<Value>> {
info!("Getting item {}", id);
let search_query = Search {
id: Some(id.to_string()),
......@@ -152,13 +137,7 @@ fn new_item_id() -> String {
.collect()
}
fn insert_property(
tx: &Transaction,
schema: &Schema,
rowid: i64,
name: &str,
json: &Value,
) -> Result<()> {
fn insert_property(tx: &Tx, schema: &Schema, rowid: i64, name: &str, json: &Value) -> Result<()> {
let dbtype = if let Some(t) = schema.property_types.get(name) {
t
} else {
......@@ -230,7 +209,7 @@ fn insert_property(
}
pub fn create_item_tx(
tx: &Transaction,
tx: &Tx,
schema: &Schema,
item: CreateItem,
pod_owner: &str,
......@@ -273,7 +252,7 @@ pub fn create_item_tx(
}
pub fn update_item_tx(
tx: &Transaction,
tx: &Tx,
schema: &Schema,
id: &str,
mut fields: HashMap<String, Value>,
......@@ -324,7 +303,7 @@ pub fn update_item_tx(
Ok(())
}
pub fn delete_item_tx(tx: &Transaction, schema: &Schema, id: &str) -> Result<()> {
pub fn delete_item_tx(tx: &Tx, schema: &Schema, id: &str) -> Result<()> {
log::debug!("Deleting item {}", id);
let mut fields = HashMap::new();
fields.insert("deleted".to_string(), true.into());
......@@ -332,7 +311,7 @@ pub fn delete_item_tx(tx: &Transaction, schema: &Schema, id: &str) -> Result<()>
}
pub fn bulk_tx(
tx: &Transaction,
tx: &Tx,
schema: &Schema,
bulk: Bulk,
pod_owner: &str,
......@@ -357,6 +336,12 @@ pub fn bulk_tx(
Ok(())
}
fn item_base_to_json(tx: &Tx, item: ItemBase, schema: &Schema) -> Result<Value> {
let mut props = get_item_properties(tx, item.rowid, schema)?;
add_item_base_properties(&mut props, item);
Ok(Value::Object(props))
}
fn add_item_base_properties(props: &mut Map<String, Value>, item: ItemBase) {
props.insert("id".to_string(), item.id.into());
props.insert("type".to_string(), item._type.into());
......@@ -369,7 +354,56 @@ fn add_item_base_properties(props: &mut Map<String, Value>, item: ItemBase) {
props.insert("deleted".to_string(), item.deleted.into());
}
pub fn search(tx: &Transaction, schema: &Schema, query: Search) -> Result<Vec<Value>> {
pub fn create_edge(tx: &Tx, query: CreateEdge) -> Result<String> {
let date = Utc::now().timestamp_millis();
let self_id = new_item_id();
let self_rowid = database_api::insert_item_base(tx, &self_id, "Edge", date, date, date, false)?;
let source = database_api::get_item_rowid(tx, &query.source)?.ok_or_else(|| Error {
code: StatusCode::NOT_FOUND,
msg: format!("Edge source not found: {}", query.source),
})?;
let target = database_api::get_item_rowid(tx, &query.target)?.ok_or_else(|| Error {
code: StatusCode::NOT_FOUND,
msg: format!("Edge target not found: {}", query.target),
})?;
database_api::insert_edge(tx, self_rowid, source, &query.name, target)?;
Ok(self_id)
}
pub fn get_edges(tx: &Tx, query: GetEdges, schema: &Schema) -> Result<Vec<Value>> {
let root_item = database_api::get_item_rowid(tx, &query.item)?.ok_or_else(|| Error {
code: StatusCode::NOT_FOUND,
msg: format!("Cannot find item id {}", query.item),
})?;
let edges = if query.direction == EdgeDirection::Outgoing {
database_api::get_outgoing_edges(tx, root_item)?
} else {
database_api::get_incoming_edges(tx, root_item)?
};
let mut result = Vec::new();
for edge in edges {
let base = database_api::get_item_base(tx, edge.item)?;
let base = base.ok_or_else(|| Error {
code: StatusCode::INTERNAL_SERVER_ERROR,
msg: format!("Edge connects to an nonexisting item.rowid {}", edge.item),
})?;
if query.expand_items {
let item_json = item_base_to_json(tx, base, schema)?;
result.push(serde_json::json!({
"name": edge.name,
"item": item_json,
}));
} else {
result.push(serde_json::json!({
"name": edge.name,
"item": { "id": base.id },
}));
}
}
Ok(result)
}
pub fn search(tx: &Tx, schema: &Schema, query: Search) -> Result<Vec<Value>> {
info!("Searching by fields {:?}", query);
if !query.other_properties.is_empty() {
return Err(Error {
......@@ -395,9 +429,7 @@ pub fn search(tx: &Transaction, schema: &Schema, query: Search) -> Result<Vec<Va
let items = database_api::search_items(tx, &database_search)?;
let mut result = Vec::new();
for item in items {
let mut properties = get_item_properties(tx, item.rowid, schema)?;
add_item_base_properties(&mut properties, item);
result.push(Value::Object(properties))
result.push(item_base_to_json(tx, item, schema)?)
}
Ok(result)
}
......
......@@ -5,13 +5,13 @@ mod command_line_interface;
mod constants;
pub mod database_api;
mod database_migrate_refinery;
pub mod database_migrate_schema;
mod database_migrate_schema;
mod error;
pub mod file_api;
mod file_api;
mod global_static;
pub mod internal_api;
mod internal_api;
mod plugin_auth_crypto;
pub mod plugin_run;
mod plugin_run;
mod schema;
mod triggers;
mod warp_api;
......
use crate::api_model::Bulk;
use crate::api_model::CreateEdge;
use crate::api_model::CreateItem;
use crate::api_model::GetEdges;
use crate::api_model::GetFile;
use crate::api_model::PayloadWrapper;
use crate::api_model::Search;
......@@ -91,6 +93,28 @@ pub async fn run_server(cli_options: CliOptions) {
respond_with_result(result)
});
let init_db = initialized_databases_arc.clone();
let get_edges = items_api
.and(warp::path!(String / "get_edges"))
.and(warp::path::end())
.and(warp::body::json())
.map(move |owner: String, body: PayloadWrapper<GetEdges>| {
let result = warp_endpoints::get_edges(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 create_edge = items_api
.and(warp::path!(String / "create_edge"))
.and(warp::path::end())
.and(warp::body::json())
.map(move |owner: String, body: PayloadWrapper<CreateEdge>| {
let result = warp_endpoints::create_edge(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 cli_options_arc_clone = cli_options_arc.clone();
let bulk_action = items_api
......@@ -116,7 +140,7 @@ pub async fn run_server(cli_options: CliOptions) {
});
let init_db = initialized_databases_arc.clone();
let search_by_fields = items_api
let search = items_api
.and(warp::path!(String / "search"))
.and(warp::path::end())
.and(warp::body::json())
......@@ -196,7 +220,9 @@ 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_by_fields.with(&headers))
.or(search.with(&headers))
.or(get_edges.with(&headers))
.or(create_edge.with(&headers))
.or(upload_file.with(&headers))
.or(get_file.with(&headers))
.or(origin_request);
......
use crate::api_model::AuthKey;
use crate::api_model::Bulk;
use crate::api_model::CreateEdge;
use crate::api_model::CreateItem;
use crate::api_model::GetEdges;
use crate::api_model::GetFile;
use crate::api_model::PayloadWrapper;
use crate::api_model::Search;
......@@ -112,6 +114,33 @@ pub fn delete_item(
})
}
pub fn create_edge(
owner: String,
init_db: &RwLock<HashSet<String>>,
body: PayloadWrapper<CreateEdge>,
) -> Result<String> {
let auth = body.auth;
let payload = body.payload;
let database_key = auth_to_database_key(auth)?;
let mut conn: Connection = check_owner_and_initialize_db(&owner, &init_db, &database_key)?;
in_transaction(&mut conn, |tx| internal_api::create_edge(&tx, payload))
}
pub fn get_edges(
owner: String,
init_db: &RwLock<HashSet<String>>,
body: PayloadWrapper<GetEdges>,
) -> Result<Vec<Value>> {
let auth = body.auth;
let payload = body.payload;
let database_key = auth_to_database_key(auth)?;
let mut conn: Connection = check_owner_and_initialize_db(&owner, &init_db, &database_key)?;
in_transaction(&mut conn, |tx| {
let schema = database_api::get_schema(&tx)?;
internal_api::get_edges(&tx, payload, &schema)
})
}
pub fn search(
owner: String,
init_db: &RwLock<HashSet<String>>,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment