hc_crud/
lib.rs

1//! Other Resources
2//!
3//! - Source code - [github.com/spartan-holochain-counsel/rust-hc-crud-caps](https://github.com/spartan-holochain-counsel/rust-hc-crud-caps/)
4//! - Cargo package - [crates.io/crates/hc_crud_caps](https://crates.io/crates/hc_crud_caps)
5//!
6
7mod entities;
8mod utils;
9
10pub use hdk_extensions::hdi;
11pub use hdk_extensions::hdi_extensions;
12pub use hdk_extensions::hdk;
13pub use hdk_extensions;
14
15use std::convert::TryFrom;
16use hdk::prelude::*;
17use hdi_extensions::{
18    summon_create_action,
19    trace_origin_root,
20};
21use hdk_extensions::{
22    must_get,
23    follow_evolutions,
24};
25
26pub use entities::{
27    Entity, EmptyEntity,
28    EntryModel,
29};
30pub use utils::{
31    now,
32    to_entry_type,
33    create_link_input,
34};
35
36
37pub type EntityId = ActionHash;
38
39
40#[derive(Debug, Serialize, Deserialize)]
41pub struct GetEntityInput {
42    pub id: ActionHash,
43}
44
45impl Into<GetEntityInput> for ActionHash {
46    fn into(self) -> GetEntityInput {
47        GetEntityInput {
48            id: self,
49        }
50    }
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct UpdateEntityInput<T> {
55    pub base: ActionHash,
56    pub properties: T,
57}
58
59impl<T> UpdateEntityInput<T> {
60    pub fn new(base: ActionHash, properties: T) -> Self {
61        UpdateEntityInput { base, properties }
62    }
63}
64
65
66
67/// Create a new entity
68pub fn create_entity<T,I,E>(entry: &T) -> ExternResult<Entity<T>>
69where
70    ScopedEntryDefIndex: for<'a> TryFrom<&'a I, Error = WasmError>,
71    EntryVisibility: for<'a> From<&'a I>,
72    Entry: TryFrom<I, Error = E>,
73    Entry: TryFrom<T, Error = E>,
74    WasmError: From<E>,
75    T: Clone + EntryModel<I>,
76{
77    let entry_hash = hash_entry( entry.to_owned() )?;
78    let action_hash = create_entry( entry.to_input() )?;
79
80    Ok(Entity {
81        id: action_hash.to_owned(),
82        address: entry_hash,
83        action: action_hash,
84        ctype: entry.get_type(),
85        content: entry.to_owned(),
86    })
87}
88
89/// Get an entity by its ID
90pub fn get_entity<I,ET>(id: &ActionHash) -> ExternResult<Entity<I>>
91where
92    I: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
93    Entry: TryFrom<I, Error = WasmError>,
94    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
95{
96    // Check if ID record has been deleted
97    must_get( id )?;
98    // ID must be a Create action
99    summon_create_action( id )?;
100
101    let latest_addr = follow_evolutions( &id )?.last().unwrap().to_owned();
102    let record = must_get( &latest_addr )?;
103    let content : I = to_entry_type( record.clone() )?;
104    let address = record
105        .action()
106        .entry_hash().unwrap(); // to_entry_type would have failed if there was not entry
107
108    Ok(Entity {
109        id: id.to_owned(),
110        action: record.action_address().to_owned(),
111        address: address.to_owned(),
112        ctype: content.get_type(),
113        content: content,
114    })
115}
116
117/// Update an entity
118pub fn update_entity<T,I,F,E>(addr: &ActionHash, callback: F) -> ExternResult<Entity<T>>
119where
120    ScopedEntryDefIndex: for<'a> TryFrom<&'a I, Error = WasmError>,
121    Entry: TryFrom<I, Error = E>,
122    Entry: TryFrom<T, Error = E>,
123    WasmError: From<E>,
124    T: TryFrom<Record, Error = WasmError>,
125    T: Clone + EntryModel<I>,
126    F: FnOnce(T, Record) -> ExternResult<T>,
127{
128    // TODO: provide automatic check that the given address is the latest one or an optional flag
129    // to indicate the intension to branch from an older update.
130    let (id,_) = trace_origin_root( &addr )?;
131    let record = must_get( addr )?;
132
133    let current : T = to_entry_type( record.clone() )?;
134    let updated_entry = callback( current, record.clone() )?;
135
136    let entry_hash = hash_entry( updated_entry.to_owned() )?;
137    let action_hash = update_entry( addr.clone(), updated_entry.to_input() )?;
138
139    Ok(Entity {
140        id: id,
141        action: action_hash,
142        address: entry_hash,
143        ctype: updated_entry.get_type(),
144        content: updated_entry,
145    })
146}
147
148/// Delete an entity
149pub fn delete_entity<T,ET>(id: &ActionHash) -> ExternResult<ActionHash>
150where
151    T: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
152    Entry: TryFrom<T, Error = WasmError>,
153    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
154{
155    // ID must be a Create action
156    summon_create_action( &id )?;
157
158    let record = must_get( id )?;
159    let _ : T = to_entry_type( record.clone() )?;
160    debug!("Deleting record of Entity ID: {}", id );
161    let delete_hash = delete_entry( id.to_owned() )?;
162
163    Ok( delete_hash )
164}
165
166
167/// Get multiple entities for a given base and link tag filter
168pub fn get_entities<T,LT,ET,B>(id: &B, link_type: LT, tag: Option<Vec<u8>>) -> ExternResult<Vec<Entity<T>>>
169where
170    T: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
171    B: Into<AnyLinkableHash> + Clone,
172    LT: LinkTypeFilterExt + Clone,
173    Entry: TryFrom<T, Error = WasmError>,
174    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
175{
176    let links = get_links(
177        create_link_input(
178            id,
179            &link_type,
180            &tag,
181        )?
182    )?;
183
184    debug!("get_entities for {} links: {:#?}", links.len(), links );
185    let list = links.into_iter()
186        .filter_map(|link| {
187            debug!("Get entity for ID: {:?}", link.target );
188            link.target.into_action_hash()
189                .and_then( |target| get_entity( &target ).ok() )
190        })
191        .collect();
192
193    Ok(list)
194}