hc_crud/
lib.rs

1//! Other Resources
2//!
3//! - Source code - [github.com/mjbrisebois/rust-hc-crud-ceps](https://github.com/mjbrisebois/rust-hc-crud-ceps/)
4//! - Cargo package - [crates.io/crates/hc_crud_ceps](https://crates.io/crates/hc_crud_ceps)
5//!
6
7mod errors;
8mod entities;
9mod utils;
10
11use std::convert::TryFrom;
12use hdk::prelude::*;
13
14pub use entities::{ Entity, EmptyEntity, EntityType, EntryModel };
15pub use errors::{ UtilsResult, UtilsError };
16pub use utils::{
17    now, find_latest_link, path_from_collection,
18    trace_action_history, to_entry_type,
19};
20
21
22
23/// Get the entity ID for any given entity EntryHash
24pub fn get_origin_address(addr: &ActionHash) -> UtilsResult<EntryHash> {
25    let chain = trace_action_history( addr )?;
26
27    // The starting 'addr' will always be in the chain so it is safe to unwrap.
28    Ok( chain.last().unwrap().1.to_owned() )
29}
30
31/// Get the record for any given EntryHash
32pub fn fetch_record(addr: &EntryHash) -> UtilsResult<(ActionHash, Record)> {
33    let record = get( addr.to_owned(), GetOptions::latest() )?
34	.ok_or( UtilsError::EntryNotFoundError(addr.to_owned(), Some("".to_string())) )?;
35
36    Ok( (record.action_address().to_owned(), record) )
37}
38
39/// Finds and returns the Action with the earliest timestamp from a list
40pub fn find_earliest_action(updates: Vec<SignedHashed<Action>>) -> Option<SignedHashed<Action>> {
41    if updates.len() == 0 {
42	None
43    }
44    else {
45	Some( updates.iter()
46            .fold( None, |acc, sh| {
47		let ts = sh.action().timestamp();
48		match acc {
49		    None => Some( (ts, sh.to_owned()) ),
50		    Some(current) => {
51			Some(match current.0 < ts {
52			    true => current,
53			    false => (ts, sh.to_owned()),
54			})
55		    }
56		}
57	    }).unwrap().1 )
58    }
59}
60
61
62/// Follow the trail of (earliest) updates and return the full Action path.
63pub fn follow_updates(hash: &ActionHash, trace: Option<Vec<ActionHash>>) -> UtilsResult<Vec<ActionHash>> {
64    let mut history = trace.unwrap_or( Vec::new() );
65    history.push( hash.to_owned() );
66
67    let details = get_details( hash.to_owned(), GetOptions::latest() )?
68	.ok_or( UtilsError::ActionNotFoundError(hash.to_owned(), Some("".to_string())) )?;
69    let updates = match details {
70	Details::Record(details) => details.updates,
71	Details::Entry(details) => details.updates,
72    };
73
74    Ok( match find_earliest_action( updates ) {
75	None => history,
76	Some(next_update) => follow_updates( next_update.action_address(), Some(history) )?,
77    })
78}
79
80/// Get the latest Record for any given entity ID
81pub fn fetch_record_latest(id: &EntryHash) -> UtilsResult<(ActionHash, Record)> {
82    let (action_hash, first_record) = fetch_record( id )?;
83
84    match first_record.action() {
85	Action::Create(_) => (),
86	_ => Err(UtilsError::NotOriginEntryError(action_hash.to_owned()))?,
87    }
88
89    let updates = follow_updates( &action_hash, None )?;
90    let latest_action_hash = updates.last().unwrap();
91    let record = get( latest_action_hash.to_owned(), GetOptions::latest() )?
92	.ok_or( UtilsError::ActionNotFoundError(action_hash.to_owned(), Some("".to_string())) )?;
93
94    Ok( (action_hash, record) )
95}
96
97
98
99/// Create a new entity
100pub fn create_entity<T,I,E>(entry: &T) -> UtilsResult<Entity<T>>
101where
102    ScopedEntryDefIndex: for<'a> TryFrom<&'a I, Error = WasmError>,
103    EntryVisibility: for<'a> From<&'a I>,
104    Entry: TryFrom<I, Error = E>,
105    Entry: TryFrom<T, Error = E>,
106    WasmError: From<E>,
107    T: Clone + EntryModel<I>,
108{
109    let entry_hash = hash_entry( entry.to_owned() )?;
110    let action_hash = create_entry( entry.to_input() )?;
111
112    Ok(Entity {
113	id: entry_hash.to_owned(),
114	address: entry_hash,
115	action: action_hash,
116	ctype: entry.get_type(),
117	content: entry.to_owned(),
118    })
119}
120
121/// Get an entity by its ID
122pub fn get_entity<I,ET>(id: &EntryHash) -> UtilsResult<Entity<I>>
123where
124    I: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
125    Entry: TryFrom<I, Error = WasmError>,
126    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
127{
128    let (_, record) = fetch_record_latest( id )?;
129    let to_type_input = record.to_owned();
130    let address = record
131	.action()
132	.entry_hash()
133	.ok_or(UtilsError::EntryNotFoundError(id.to_owned(), None))?;
134
135    let content : I = to_entry_type( to_type_input )?;
136
137    Ok(Entity {
138	id: id.to_owned(),
139	action: record.action_address().to_owned(),
140	address: address.to_owned(),
141	ctype: content.get_type(),
142	content: content,
143    })
144}
145
146/// Update an entity
147pub fn update_entity<T,I,F,E>(addr: &ActionHash, callback: F) -> UtilsResult<Entity<T>>
148where
149    ScopedEntryDefIndex: for<'a> TryFrom<&'a I, Error = WasmError>,
150    Entry: TryFrom<I, Error = E>,
151    Entry: TryFrom<T, Error = E>,
152    WasmError: From<E>,
153    T: TryFrom<Record, Error = WasmError>,
154    T: Clone + EntryModel<I>,
155    F: FnOnce(T, Record) -> UtilsResult<T>,
156{
157    // TODO: provide automatic check that the given address is the latest one or an optional flag
158    // to indicate the intension to branch from an older update.
159    let id = get_origin_address( &addr )?;
160    let record = get( addr.to_owned(), GetOptions::latest() )?
161	.ok_or( UtilsError::ActionNotFoundError(addr.to_owned(), Some("Given origin for update is not found".to_string())) )?;
162
163    let current : T = to_entry_type( record.clone() )?;
164    let updated_entry = callback( current, record.clone() )?;
165
166    let entry_hash = hash_entry( updated_entry.to_owned() )?;
167    let action_hash = update_entry( addr.to_owned(), updated_entry.to_input() )?;
168
169    Ok(Entity {
170	id: id,
171	action: action_hash,
172	address: entry_hash,
173	ctype: updated_entry.get_type(),
174	content: updated_entry,
175    })
176}
177
178/// Delete an entity
179pub fn delete_entity<T,ET>(id: &EntryHash) -> UtilsResult<ActionHash>
180where
181    T: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
182    Entry: TryFrom<T, Error = WasmError>,
183    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
184{
185    let (action_hash, record) = fetch_record( id )?;
186    let _ : T = to_entry_type( record )?;
187    let delete_hash = delete_entry( action_hash )?;
188
189    Ok( delete_hash )
190}
191
192
193/// Get multiple entities for a given base and link tag filter
194pub fn get_entities<T,LT,ET>(id: &EntryHash, link_type: LT, tag: Option<Vec<u8>>) -> UtilsResult<Vec<Entity<T>>>
195where
196    T: TryFrom<Record, Error = WasmError> + Clone + EntryModel<ET>,
197    LT: LinkTypeFilterExt,
198    Entry: TryFrom<T, Error = WasmError>,
199    ScopedEntryDefIndex: for<'a> TryFrom<&'a ET, Error = WasmError>,
200{
201    let links_result = get_links(
202        id.to_owned(),
203	link_type,
204	tag.map( |tag| LinkTag::new( tag ) )
205    );
206    debug!("get_entities: {:?}", links_result );
207    let links = links_result?;
208
209    let list = links.into_iter()
210	.filter_map(|link| {
211	    link.target.into_entry_hash()
212		.and_then( |target| get_entity( &target ).ok() )
213	})
214	.collect();
215
216    Ok(list)
217}