├── .github └── workflows │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── README.md ├── run-test.sh └── src ├── crud.rs ├── datetime_queries ├── fetch_by_day.rs ├── fetch_by_hour.rs ├── fetch_by_time.rs ├── fetch_entries_from_day_to_day.rs ├── fetch_entries_from_day_to_hour.rs ├── fetch_entries_from_hour_to_day.rs ├── fetch_entries_from_hour_to_hour.rs ├── fetch_in_time_range.rs ├── fetchers.rs ├── inputs.rs ├── mod.rs └── utils.rs ├── lib.rs ├── modify_chain ├── do_create.rs ├── do_delete.rs ├── do_fetch.rs ├── do_update.rs ├── mod.rs └── utils.rs ├── retrieval ├── fetch_entries.rs ├── fetch_links.rs ├── get_latest_for_entry.rs ├── inputs.rs ├── mod.rs └── utils.rs ├── signals.rs └── wire_record.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # name: Cargo tests 2 | 3 | # on: [ push, pull_request ] 4 | 5 | # jobs: 6 | # cargo_build_wasm: 7 | # runs-on: ${{ matrix.os }} 8 | # strategy: 9 | # matrix: 10 | # os: [ubuntu-latest, macos-latest] 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: actions-rs/toolchain@v1 14 | # with: 15 | # toolchain: stable 16 | # target: wasm32-unknown-unknown 17 | # - run: cargo build --release --target wasm32-unknown-unknown 18 | # cargo_test: 19 | # runs-on: ubuntu-latest 20 | 21 | # steps: 22 | # - uses: actions/checkout@v2 23 | # - uses: actions-rs/toolchain@v1 24 | # with: 25 | # toolchain: stable 26 | # - run: ./run-test.sh 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.checkOnSave.features": null, 3 | "rust-analyzer.cargo.features": [ 4 | 5 | // "mock" 6 | ] 7 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hdk_crud" 3 | description = "A library to help quickly and easily create a consistent set of create-read-update-delete functions for an Entry type in Holochain, complete with signaling options" 4 | version = "0.14.0" 5 | license = "CAL-1.0" 6 | repository = "https://github.com/lightningrodlabs/hdk_crud" 7 | authors = ["Connor Turland ", "Wesley Finck ), 35 | /// } 36 | /// impl From> for SignalTypes { 37 | /// fn from(value: ActionSignal) -> Self { 38 | /// SignalTypes::Example(value) 39 | /// } 40 | /// } 41 | /// 42 | /// pub fn recv_remote_signal(signal: ExternIO) -> ExternResult<()> { 43 | /// Ok(emit_signal(&signal)?) 44 | /// } 45 | /// 46 | /// pub fn get_peers() -> ExternResult> { 47 | /// Ok(Vec::new()) 48 | /// } 49 | /// 50 | /// crud!( 51 | /// Example, 52 | /// EntryTypes, 53 | /// EntryTypes::Example, 54 | /// LinkTypes, 55 | /// LinkTypes::All, 56 | /// example, 57 | /// "example", 58 | /// get_peers, 59 | /// SignalTypes 60 | /// ); 61 | /// ``` 62 | #[macro_export] 63 | macro_rules! crud { 64 | ( 65 | $crud_type:ident, $entry_types:ident, $entry_type:expr, $link_types:ident, $link_type:expr, $i:ident, $path:expr, $get_peers:ident, $signal_type:ident 66 | ) => { 67 | ::paste::paste! { 68 | 69 | /// This is the &str that can be passed into Path to 70 | /// find all the entries created using these create functions 71 | /// which are linked off of this Path. 72 | pub const [<$i:upper _PATH>]: &str = $path; 73 | 74 | /// Retrieve the Path for these entry types 75 | /// to which all entries are linked 76 | pub fn [](link_type: TY) -> ExternResult 77 | where 78 | ScopedLinkType: TryFrom, 79 | WasmError: From, 80 | { 81 | Path::from([<$i:upper _PATH>]).typed(link_type) 82 | } 83 | 84 | #[doc ="This is what is expected by a call to [update_" $path "]"] 85 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, SerializedBytes)] 86 | #[serde(rename_all = "camelCase")] 87 | pub struct [<$crud_type UpdateInput>] { 88 | pub entry: $crud_type, 89 | pub action_hash: ::holo_hash::ActionHashB64, 90 | } 91 | 92 | /* 93 | CREATE 94 | */ 95 | 96 | #[cfg(not(feature = "exclude_zome_fns"))] 97 | /// This is the exposed/public Zome function for creating an entry of this type. 98 | /// This will create an entry and link it off the main Path. 99 | /// It will send a signal of this event 100 | /// to all peers returned by the `get_peers` call given during the macro call to `crud!` 101 | #[hdk_extern] 102 | pub fn [](entry: $crud_type) -> ExternResult<$crate::wire_record::WireRecord<[<$crud_type>]>> { 103 | let do_create = $crate::modify_chain::do_create::DoCreate {}; 104 | // wrap it in its EntryTypes variant 105 | let full_entry = $entry_type(entry.clone()); 106 | do_create.do_create::<$entry_types, $crud_type, ::hdk::prelude::WasmError, $signal_type, $link_types> ( 107 | full_entry, 108 | entry, 109 | Some($crate::modify_chain::do_create::TypedPathOrEntryHash::TypedPath([< get_ $i _path >]($link_type)?)), 110 | $path.to_string(), 111 | $link_type, 112 | Some($get_peers()?), 113 | None, 114 | ) 115 | } 116 | 117 | /* 118 | READ 119 | */ 120 | 121 | #[cfg(not(feature = "exclude_zome_fns"))] 122 | /// This is the exposed/public Zome function for either fetching ALL or a SPECIFIC list of the entries of the type. 123 | /// No signals will be sent as a result of calling this. 124 | /// Notice that it pluralizes the value of `$i`, the second argument to the crud! macro call. 125 | #[hdk_extern] 126 | pub fn [](fetch_options: $crate::retrieval::inputs::FetchOptions) -> ExternResult]>>> { 127 | let do_fetch = $crate::modify_chain::do_fetch::DoFetch {}; 128 | let fetch_entries = $crate::retrieval::fetch_entries::FetchEntries {}; 129 | let fetch_links = $crate::retrieval::fetch_links::FetchLinks {}; 130 | let get_latest = $crate::retrieval::get_latest_for_entry::GetLatestEntry {}; 131 | let link_type_filter = LinkTypeFilter::try_from($link_type)?; 132 | do_fetch.do_fetch::<$crud_type, ::hdk::prelude::WasmError>( 133 | &fetch_entries, 134 | &fetch_links, 135 | &get_latest, 136 | fetch_options, 137 | GetOptions::network(), 138 | link_type_filter, 139 | None, // link_tag 140 | [< get_ $i _path >]($link_type)?, 141 | ) 142 | } 143 | 144 | /* 145 | UPDATE 146 | */ 147 | 148 | #[cfg(not(feature = "exclude_zome_fns"))] 149 | /// This is the exposed/public Zome function for creating an entry of this type. 150 | /// This will add an update to an entry. 151 | /// It will send a signal of this event 152 | /// to all peers returned by the `get_peers` call given during the macro call to `crud!` 153 | #[hdk_extern] 154 | pub fn [](update: [<$crud_type UpdateInput>]) -> ExternResult<$crate::wire_record::WireRecord<[<$crud_type>]>> { 155 | let do_update = $crate::modify_chain::do_update::DoUpdate {}; 156 | do_update.do_update::<$crud_type, ::hdk::prelude::WasmError, $signal_type, $link_types>( 157 | update.entry, 158 | update.action_hash, 159 | $path.to_string(), 160 | $link_type, 161 | Some($get_peers()?), 162 | None, 163 | ) 164 | } 165 | 166 | /* 167 | DELETE 168 | */ 169 | 170 | #[cfg(not(feature = "exclude_zome_fns"))] 171 | /// This is the exposed/public Zome function for archiving an entry of this type. 172 | /// This will mark the entry at `address` as "deleted". 173 | #[doc="It will no longer be returned by [fetch_" $i "s]."] 174 | /// It will send a signal of this event 175 | /// to all peers returned by the `get_peers` call given during the macro call to `crud!` 176 | #[hdk_extern] 177 | pub fn [](address: ::holo_hash::ActionHashB64) -> ExternResult<::holo_hash::ActionHashB64> { 178 | let do_delete = $crate::modify_chain::do_delete::DoDelete {}; 179 | do_delete.do_delete::<$crud_type, ::hdk::prelude::WasmError, $signal_type>( 180 | address, 181 | $path.to_string(), 182 | Some($get_peers()?), 183 | ) 184 | } 185 | } 186 | }; 187 | } 188 | 189 | /// Take a look at this module to get a concrete example 190 | /// of what you need to pass to the crud! macro, as well 191 | /// as what you'll get back out of it. 192 | /// Anything that says "NOT GENERATED" is not 193 | /// generated by the crud! macro call, and the rest is. 194 | /// It will generate 4 public Zome functions 195 | /// The 4 Zome functions in this example would be: 196 | /// [create_example](example::create_example), [fetch_examples](example::fetch_examples), [update_example](example::update_example), and [delete_example](example::delete_example). 197 | #[cfg(not(feature = "no_example"))] 198 | pub mod example { 199 | use crate::signals::*; 200 | use hdk::prelude::*; 201 | 202 | /// NOT GENERATED 203 | /// This is our example hdk_entry entry 204 | /// type definition. 205 | #[hdk_entry_helper] 206 | #[derive(Clone, PartialEq)] 207 | pub struct Example { 208 | pub number: i32, 209 | } 210 | 211 | #[hdk_entry_types] 212 | #[unit_enum(UnitEntryTypes)] 213 | #[derive(Clone)] 214 | pub enum EntryTypes { 215 | #[entry_type(required_validations = 5)] 216 | Example(Example), 217 | } 218 | 219 | #[hdk_link_types] 220 | pub enum LinkTypes { 221 | All, 222 | } 223 | 224 | /// NOT GENERATED 225 | /// A high level signal type to unify all the entry type specific 226 | /// signal types. Must implement the `From>` trait 227 | #[derive(Debug, Serialize, Deserialize, SerializedBytes)] 228 | // untagged because the useful tagging is done internally on the ActionSignal objects 229 | #[serde(untagged)] 230 | pub enum SignalTypes { 231 | Example(ActionSignal), 232 | } 233 | impl From> for SignalTypes { 234 | fn from(value: ActionSignal) -> Self { 235 | SignalTypes::Example(value) 236 | } 237 | } 238 | 239 | /// NOT GENERATED 240 | /// Signal Receiver 241 | /// (forwards signals to the UI) 242 | /// would be handling a 243 | pub fn recv_remote_signal(signal: ExternIO) -> ExternResult<()> { 244 | Ok(emit_signal(&signal)?) 245 | } 246 | 247 | /// NOT GENERATED 248 | /// This handles the fetching of a list of peers to which to send 249 | /// signals. In this example it's an empty list. Your function 250 | /// signature should match this function signature. 251 | pub fn get_peers() -> ExternResult> { 252 | Ok(Vec::new()) 253 | } 254 | 255 | #[cfg(not(feature = "mock"))] 256 | crud!( 257 | Example, 258 | EntryTypes, 259 | EntryTypes::Example, 260 | LinkTypes, 261 | LinkTypes::All, 262 | example, 263 | "example", 264 | get_peers, 265 | SignalTypes 266 | ); 267 | } 268 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_by_day.rs: -------------------------------------------------------------------------------- 1 | use crate::datetime_queries::inputs::FetchEntriesTime; 2 | use crate::datetime_queries::utils::{day_path_from_date, err, get_last_component_string}; 3 | use crate::wire_record::WireRecord; 4 | use hdk::prelude::*; 5 | 6 | #[cfg(feature = "mock")] 7 | use ::mockall::automock; 8 | 9 | #[cfg(not(feature = "mock"))] 10 | use crate::datetime_queries::fetch_by_hour::FetchByHour; 11 | #[cfg(not(feature = "mock"))] 12 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 13 | 14 | #[cfg(feature = "mock")] 15 | use crate::datetime_queries::fetch_by_hour::MockFetchByHour as FetchByHour; 16 | #[cfg(feature = "mock")] 17 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 18 | #[derive(Clone)] 19 | pub struct FetchByDay {} 20 | #[cfg_attr(feature = "mock", automock)] 21 | impl FetchByDay { 22 | /// fetches all entries linked to a time path index for a certain day 23 | pub fn fetch_entries_by_day< 24 | EntryType: 'static + TryFrom, 25 | TY, 26 | E, 27 | >( 28 | &self, 29 | fetch_by_hour: &FetchByHour, 30 | get_latest_entry: &GetLatestEntry, 31 | link_type_filter: LinkTypeFilter, 32 | link_type: TY, 33 | time: FetchEntriesTime, 34 | base_component: String, 35 | ) -> Result>, WasmError> 36 | where 37 | ScopedLinkType: TryFrom, 38 | TY: Clone, 39 | WasmError: From, 40 | { 41 | let path = day_path_from_date( 42 | link_type.clone(), 43 | base_component.clone(), 44 | time.year, 45 | time.month, 46 | time.day, 47 | )?; 48 | // TODO: wrap in path.exists which would add extra hdk calls to be mocked in the test 49 | let children = path.children()?; 50 | let entries = children 51 | .into_iter() 52 | .map(|hour_link| { 53 | let hour_str = get_last_component_string(hour_link.tag)?; 54 | let hour = hour_str.parse::().or(Err(err("Invalid path")))?; 55 | fetch_by_hour.fetch_entries_by_hour::( 56 | &get_latest_entry, 57 | link_type_filter.clone(), 58 | link_type.clone(), 59 | time.year, 60 | time.month, 61 | time.day, 62 | hour, 63 | base_component.clone(), 64 | ) 65 | }) 66 | .filter_map(Result::ok) 67 | .flatten() 68 | .collect(); 69 | Ok(entries) 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use crate::crud::example::Example; 76 | use crate::datetime_queries::fetch_by_hour; 77 | use crate::datetime_queries::inputs::FetchEntriesTime; 78 | use crate::retrieval::get_latest_for_entry; 79 | use crate::wire_record::WireRecord; 80 | use ::fixt::prelude::*; 81 | use hdk::hash_path::path::{Component, DHT_PREFIX}; 82 | use hdk::prelude::*; 83 | use holochain_types::prelude::RecordFixturator; 84 | 85 | #[test] 86 | fn test_fetch_entries_by_day() { 87 | let mut mock_hdk = MockHdkT::new(); 88 | 89 | // when fetch_entries_by_day calls path.children(), assuming the path already exists, the following hdk 90 | // functions are called: hash_entry x4, get, get_links 91 | 92 | // set up for the first expected hash_entry call 93 | 94 | let path = Path::from("create.2021-10-15"); 95 | let path_hash = fixt!(EntryHash); 96 | let path_entry = PathEntry::new(path_hash.clone()); 97 | let path_entry_hash = fixt!(EntryHash); 98 | mock_hdk 99 | .expect_hash_entry() 100 | .with(mockall::predicate::eq( 101 | Entry::try_from(path.clone()).unwrap(), 102 | )) 103 | .times(1) 104 | .return_const(Ok(path_hash.clone())); 105 | 106 | mock_hdk 107 | .expect_hash_entry() 108 | .with(mockall::predicate::eq( 109 | Entry::try_from(path_entry.clone()).unwrap(), 110 | )) 111 | .times(1) 112 | .return_const(Ok(path_entry_hash.clone())); 113 | mock_hdk 114 | .expect_hash_entry() 115 | .with(mockall::predicate::eq( 116 | Entry::try_from(path.clone()).unwrap(), 117 | )) 118 | .times(1) 119 | .return_const(Ok(path_hash.clone())); 120 | 121 | mock_hdk 122 | .expect_hash_entry() 123 | .with(mockall::predicate::eq( 124 | Entry::try_from(path_entry.clone()).unwrap(), 125 | )) 126 | .times(1) 127 | .return_const(Ok(path_entry_hash.clone())); 128 | 129 | // // set up for expected get call 130 | let path_get_input = vec![GetInput::new( 131 | AnyDhtHash::from(path_entry_hash.clone()), 132 | GetOptions::local(), 133 | )]; 134 | let expected_get_output = vec![Some(fixt!(Record))]; // this should return the path 135 | mock_hdk 136 | .expect_get() 137 | .with(mockall::predicate::eq(path_get_input)) 138 | .times(1) 139 | .return_const(Ok(expected_get_output)); 140 | 141 | // set up input for get links, the second parameter is the default used by the Holochain code 142 | let get_links_input = vec![GetLinksInput::new( 143 | path_entry_hash, 144 | Some(holochain_zome_types::link::LinkTag::new([DHT_PREFIX])), 145 | )]; 146 | 147 | // creating an expected output of get_links, which is a Vec, and Links is a Vec 148 | // since the link tag is used to get the hour component from the path, it must be constructed properly 149 | let hour_component = Component::from(String::from("10")); 150 | let link_tag = LinkTag::new( 151 | [DHT_PREFIX] 152 | .iter() 153 | .chain( 154 | >::from(UnsafeBytes::from( 155 | SerializedBytes::try_from(hour_component).unwrap(), 156 | )) 157 | .iter(), 158 | ) 159 | .cloned() 160 | .collect::>(), 161 | ); 162 | 163 | let link_output = Link { 164 | target: fixt![EntryHash], 165 | timestamp: fixt![Timestamp], 166 | tag: link_tag, 167 | create_link_hash: fixt![ActionHash], 168 | }; 169 | 170 | // here we are assuming there is only one hour component to the day path, however if we wanted to 171 | // make sure the code properly cycles through each hour component, we would add extra Link records 172 | // to the below vector 173 | let get_links_output = vec![vec![link_output]]; 174 | 175 | mock_hdk 176 | .expect_get_links() 177 | .with(mockall::predicate::eq(get_links_input)) 178 | .times(1) 179 | .return_const(Ok(get_links_output)); 180 | 181 | // initializing expected inputs and outputs for the mocked fetch_entries_by_hour 182 | let fetch_time = FetchEntriesTime { 183 | year: 2021, 184 | month: 10 as u32, 185 | day: 15 as u32, 186 | hour: None, 187 | }; 188 | let base_component = "create".to_string(); 189 | let hour_entry = WireRecord:: { 190 | action_hash: fixt![ActionHashB64], 191 | entry_hash: fixt![EntryHashB64], 192 | entry: Example { number: 1 }, 193 | created_at: fixt![Timestamp], 194 | updated_at: fixt![Timestamp], 195 | }; 196 | let hour_entries: Vec> = vec![hour_entry]; 197 | // set up a mock of fetch_entries_by_hour 198 | let mut mock_queries = fetch_by_hour::MockFetchByHour::new(); 199 | let mock_latest_entry = get_latest_for_entry::MockGetLatestEntry::new(); 200 | mock_queries 201 | .expect_fetch_entries_by_hour::() 202 | .with( 203 | // MockGetLatestEntry does not implement PartialEq so can't be compared 204 | mockall::predicate::always(), 205 | mockall::predicate::eq(fetch_time.year), 206 | mockall::predicate::eq(fetch_time.month), 207 | mockall::predicate::eq(fetch_time.day), 208 | mockall::predicate::eq(10 as u32), 209 | mockall::predicate::eq(base_component.clone()), 210 | ) 211 | .times(1) 212 | .return_const(Ok(hour_entries.clone())); 213 | 214 | set_hdk(mock_hdk); 215 | let fetch_by_day = super::FetchByDay {}; 216 | let result = fetch_by_day.fetch_entries_by_day::( 217 | &mock_queries, 218 | &mock_latest_entry, 219 | fetch_time, 220 | base_component, 221 | ); 222 | assert_eq!(result, Ok(hour_entries)); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_by_hour.rs: -------------------------------------------------------------------------------- 1 | use crate::datetime_queries::utils::hour_path_from_date; 2 | use crate::wire_record::WireRecord; 3 | use hdk::prelude::*; 4 | 5 | #[cfg(feature = "mock")] 6 | use ::mockall::automock; 7 | use std::convert::identity; 8 | 9 | #[cfg(not(feature = "mock"))] 10 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 11 | #[cfg(feature = "mock")] 12 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 13 | pub struct FetchByHour {} 14 | #[cfg_attr(feature = "mock", automock)] 15 | impl FetchByHour { 16 | /// fetches all entries linked to a time path index for a particular hour on a specific day 17 | pub fn fetch_entries_by_hour< 18 | EntryType: 'static + TryFrom, 19 | TY, 20 | E, 21 | >( 22 | &self, 23 | get_latest_entry: &GetLatestEntry, 24 | link_type_filter: LinkTypeFilter, 25 | link_type: TY, 26 | year: i32, 27 | month: u32, 28 | day: u32, 29 | hour: u32, 30 | base_component: String, 31 | ) -> Result>, WasmError> 32 | where 33 | ScopedLinkType: TryFrom, 34 | TY: Clone, 35 | WasmError: From, 36 | { 37 | let path = hour_path_from_date(link_type, base_component.clone(), year, month, day, hour)?; 38 | let input = GetLinksInputBuilder::try_new(path.path_entry_hash()?, link_type_filter)?; 39 | let links = get_links(input.build())?; 40 | 41 | let entries: Vec> = links 42 | .into_iter() 43 | .map(|link| { 44 | get_latest_entry.get_latest_for_entry::( 45 | link.target.try_into().map_err(|_| { 46 | wasm_error!(WasmErrorInner::Guest("Target is not an entry".to_string())) 47 | })?, 48 | GetOptions::network(), 49 | ) 50 | }) 51 | .filter_map(Result::ok) 52 | .filter_map(identity) 53 | .map(|x| WireRecord::from(x)) 54 | .collect::>>(); 55 | Ok(entries) 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use crate::crud::example::Example; 62 | use crate::retrieval::get_latest_for_entry; 63 | use crate::wire_record::WireRecord; 64 | use ::fixt::prelude::*; 65 | use hdk::prelude::*; 66 | 67 | #[test] 68 | fn test_fetch_entries_by_hour() { 69 | let mut mock_hdk = MockHdkT::new(); 70 | 71 | // set up for the first expected hash_entry call 72 | let path = Path::from("create.2021-10-15.10"); 73 | let path_hash = fixt!(EntryHash); 74 | let path_entry = PathEntry::new(path_hash.clone()); 75 | let path_entry_hash = fixt!(EntryHash); 76 | mock_hdk 77 | .expect_hash_entry() 78 | .with(mockall::predicate::eq( 79 | Entry::try_from(path.clone()).unwrap(), 80 | )) 81 | .times(1) 82 | .return_const(Ok(path_hash.clone())); 83 | 84 | mock_hdk 85 | .expect_hash_entry() 86 | .with(mockall::predicate::eq( 87 | Entry::try_from(path_entry.clone()).unwrap(), 88 | )) 89 | .times(1) 90 | .return_const(Ok(path_entry_hash.clone())); 91 | 92 | let get_links_input = vec![GetLinksInput::new(path_entry_hash, None, None)]; 93 | 94 | // creating an expected output of get_links, which is a Vec, and Links is a Vec 95 | let bytes: Vec = "10".try_into().unwrap(); 96 | let link_tag: LinkTag = LinkTag::new(bytes); 97 | 98 | let link_output = Link { 99 | target: fixt![EntryHash], 100 | timestamp: fixt![Timestamp], 101 | tag: link_tag, 102 | create_link_hash: fixt![ActionHash], 103 | }; 104 | 105 | let get_links_output = vec![vec![link_output.clone()]]; 106 | 107 | mock_hdk 108 | .expect_get_links() 109 | .with(mockall::predicate::eq(get_links_input)) 110 | .times(1) 111 | .return_const(Ok(get_links_output)); 112 | 113 | let get_latest_output = Some(WireRecord:: { 114 | action_hash: fixt![ActionHashB64], 115 | entry_hash: fixt![EntryHashB64], 116 | entry: Example { number: 1 }, 117 | created_at: fixt![Timestamp], 118 | updated_at: fixt![Timestamp], 119 | }); 120 | 121 | // set up a mock of get_latest_for_entry 122 | let mut mock_get_latest = get_latest_for_entry::MockGetLatestEntry::new(); 123 | mock_get_latest 124 | .expect_get_latest_for_entry::() 125 | .with( 126 | mockall::predicate::eq(link_output.target), 127 | mockall::predicate::eq(GetOptions::network()), 128 | ) 129 | .times(1) 130 | .return_const(Ok(get_latest_output.clone())); 131 | 132 | set_hdk(mock_hdk); 133 | let base_component = "create".to_string(); 134 | let fetch_by_hour = super::FetchByHour {}; 135 | let result = fetch_by_hour.fetch_entries_by_hour::( 136 | &mock_get_latest, 137 | 2021, 138 | 10 as u32, 139 | 15 as u32, 140 | 10 as u32, 141 | base_component, 142 | ); 143 | let output = vec![WireRecord::from(get_latest_output.unwrap())]; 144 | assert_eq!(result, Ok(output)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_by_time.rs: -------------------------------------------------------------------------------- 1 | use crate::datetime_queries::inputs::FetchEntriesTime; 2 | use crate::wire_record::WireRecord; 3 | use hdk::prelude::*; 4 | 5 | #[cfg(not(feature = "mock"))] 6 | use crate::datetime_queries::fetch_by_day::FetchByDay; 7 | #[cfg(not(feature = "mock"))] 8 | use crate::datetime_queries::fetch_by_hour::FetchByHour; 9 | #[cfg(not(feature = "mock"))] 10 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 11 | 12 | #[cfg(feature = "mock")] 13 | use crate::datetime_queries::fetch_by_day::MockFetchByDay as FetchByDay; 14 | #[cfg(feature = "mock")] 15 | use crate::datetime_queries::fetch_by_hour::MockFetchByHour as FetchByHour; 16 | #[cfg(feature = "mock")] 17 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 18 | 19 | /// fetches all entries linked to a time path index for either a specific day or hour of a day 20 | pub fn fetch_entries_by_time< 21 | EntryType: 'static + TryFrom, 22 | TY, 23 | E, 24 | >( 25 | fetch_by_day: &FetchByDay, 26 | fetch_by_hour: &FetchByHour, 27 | get_latest_entry: &GetLatestEntry, 28 | link_type_filter: LinkTypeFilter, 29 | link_type: TY, 30 | time: FetchEntriesTime, 31 | base_component: String, 32 | ) -> Result>, WasmError> 33 | where 34 | ScopedLinkType: TryFrom, 35 | TY: Clone, 36 | WasmError: From, 37 | { 38 | Ok(match time.hour { 39 | None => fetch_by_day.fetch_entries_by_day( 40 | &fetch_by_hour, 41 | &get_latest_entry, 42 | link_type_filter, 43 | link_type, 44 | time, 45 | base_component, 46 | ), 47 | Some(h) => fetch_by_hour.fetch_entries_by_hour( 48 | &get_latest_entry, 49 | link_type_filter, 50 | link_type, 51 | time.year, 52 | time.month, 53 | time.day, 54 | h, 55 | base_component, 56 | ), 57 | }?) 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use crate::crud::example::Example; 63 | use crate::datetime_queries::inputs::FetchEntriesTime; 64 | use crate::datetime_queries::{fetch_by_day, fetch_by_hour}; 65 | use crate::retrieval::get_latest_for_entry; 66 | use crate::wire_record::WireRecord; 67 | use ::fixt::prelude::*; 68 | use hdk::prelude::*; 69 | 70 | #[test] 71 | fn test_fetch_by_time_day() { 72 | // when calling fetch_entries_by_time without 73 | // an 'hour' then verify that it calls 74 | // fetch_entries_by_day 75 | 76 | let fetch_time = FetchEntriesTime { 77 | year: 2021, 78 | month: 10 as u32, 79 | day: 15 as u32, 80 | hour: None, 81 | }; 82 | 83 | let base_component = "create".to_string(); 84 | let wire_record = WireRecord:: { 85 | action_hash: fixt![ActionHashB64], 86 | entry_hash: fixt![EntryHashB64], 87 | entry: Example { number: 1 }, 88 | created_at: fixt![Timestamp], 89 | updated_at: fixt![Timestamp], 90 | }; 91 | let wire_vec: Vec> = vec![wire_record]; 92 | let mut mock_fetch_by_day = fetch_by_day::MockFetchByDay::new(); 93 | 94 | mock_fetch_by_day 95 | .expect_fetch_entries_by_day::() 96 | .with( 97 | mockall::predicate::always(), 98 | mockall::predicate::always(), 99 | mockall::predicate::eq(fetch_time.clone()), 100 | mockall::predicate::eq(base_component.clone()), 101 | ) 102 | .times(1) 103 | .return_const(Ok(wire_vec.clone())); 104 | 105 | let fetch_by_hour = fetch_by_hour::MockFetchByHour::new(); 106 | let get_latest_entry = get_latest_for_entry::MockGetLatestEntry::new(); 107 | let result = super::fetch_entries_by_time::( 108 | &mock_fetch_by_day, 109 | &fetch_by_hour, 110 | &get_latest_entry, 111 | fetch_time, 112 | base_component, 113 | ); 114 | assert_eq!(result, Ok(wire_vec)); 115 | } 116 | #[test] 117 | fn test_fetch_by_time_hour() { 118 | // when calling fetch_entries_by_time with 119 | // an 'hour' then verify that it calls 120 | // fetch_entries_by_hour 121 | 122 | let fetch_time = FetchEntriesTime { 123 | year: 2021, 124 | month: 10 as u32, 125 | day: 15 as u32, 126 | hour: Some(10 as u32), 127 | }; 128 | 129 | let base_component = "create".to_string(); 130 | let wire_record = WireRecord:: { 131 | action_hash: fixt![ActionHashB64], 132 | entry_hash: fixt![EntryHashB64], 133 | entry: Example { number: 1 }, 134 | created_at: fixt![Timestamp], 135 | updated_at: fixt![Timestamp], 136 | }; 137 | let wire_vec: Vec> = vec![wire_record]; 138 | let mock_fetch_by_day = fetch_by_day::MockFetchByDay::new(); 139 | 140 | let mut mock_fetch_by_hour = fetch_by_hour::MockFetchByHour::new(); 141 | mock_fetch_by_hour 142 | .expect_fetch_entries_by_hour::() 143 | .with( 144 | mockall::predicate::always(), 145 | mockall::predicate::eq(fetch_time.year), 146 | mockall::predicate::eq(fetch_time.month), 147 | mockall::predicate::eq(fetch_time.day), 148 | mockall::predicate::eq(fetch_time.hour.unwrap()), 149 | mockall::predicate::eq(base_component.clone()), 150 | ) 151 | .times(1) 152 | .return_const(Ok(wire_vec.clone())); 153 | let get_latest_entry = get_latest_for_entry::MockGetLatestEntry::new(); 154 | let result = super::fetch_entries_by_time::( 155 | &mock_fetch_by_day, 156 | &mock_fetch_by_hour, 157 | &get_latest_entry, 158 | fetch_time, 159 | base_component, 160 | ); 161 | assert_eq!(result, Ok(wire_vec)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_entries_from_day_to_day.rs: -------------------------------------------------------------------------------- 1 | use super::fetchers::Fetchers; 2 | use super::inputs::FetchEntriesTime; 3 | use crate::wire_record::WireRecord; 4 | use chrono::Duration; 5 | use hdk::prelude::*; 6 | 7 | #[cfg(feature = "mock")] 8 | use ::mockall::automock; 9 | 10 | #[derive(Clone)] 11 | pub struct FetchByDayDay {} 12 | #[cfg_attr(feature = "mock", automock)] 13 | impl FetchByDayDay { 14 | pub fn fetch_entries_from_day_to_day< 15 | EntryType: 'static + TryFrom, 16 | TY, 17 | E, 18 | >( 19 | &self, 20 | fetchers: &Fetchers, 21 | link_type_filter: LinkTypeFilter, 22 | link_type: TY, 23 | start: FetchEntriesTime, 24 | end: FetchEntriesTime, 25 | base_component: String, 26 | ) -> Result>, WasmError> 27 | where 28 | ScopedLinkType: TryFrom, 29 | TY: Clone, 30 | WasmError: From, 31 | { 32 | let mut dt = start.to_date_time(); 33 | let mut entries = Vec::new(); 34 | let end = end.to_date_time(); 35 | while dt <= end { 36 | entries.push(fetchers.day.fetch_entries_by_day::( 37 | &fetchers.hour, 38 | &fetchers.get_latest, 39 | link_type_filter.clone(), 40 | link_type.clone(), 41 | FetchEntriesTime::from_date_time(dt.clone()), 42 | base_component.clone(), 43 | )); 44 | dt = dt + Duration::days(1); 45 | } 46 | Ok(entries 47 | .into_iter() 48 | .filter_map(Result::ok) 49 | .flatten() 50 | .collect()) 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use crate::crud::example::Example; 57 | use crate::datetime_queries::fetchers::Fetchers; 58 | use crate::datetime_queries::inputs::FetchEntriesTime; 59 | 60 | use crate::wire_record::WireRecord; 61 | use ::fixt::prelude::*; 62 | use hdk::prelude::*; 63 | #[test] 64 | fn test_fetch_entries_from_day_to_day() { 65 | let start_time = FetchEntriesTime { 66 | year: 2021, 67 | month: 10 as u32, 68 | day: 20 as u32, 69 | hour: None, 70 | }; 71 | let end_time = FetchEntriesTime { 72 | year: 2021, 73 | month: 10 as u32, 74 | day: 21 as u32, 75 | hour: None, 76 | }; 77 | let base_component = "create".to_string(); 78 | let wire_record = WireRecord:: { 79 | action_hash: fixt![ActionHashB64], 80 | entry_hash: fixt![EntryHashB64], 81 | entry: Example { number: 1 }, 82 | created_at: fixt![Timestamp], 83 | updated_at: fixt![Timestamp], 84 | }; 85 | let wire_vec: Vec> = vec![wire_record.clone()]; 86 | let wire_vec2 = vec![wire_record.clone(), wire_record.clone()]; 87 | 88 | let mut mock_fetchers = Fetchers::default(); 89 | // fetch_entries_by_day should be called for each day in the range 90 | mock_fetchers 91 | .day 92 | .expect_fetch_entries_by_day::() 93 | .with( 94 | mockall::predicate::always(), 95 | mockall::predicate::always(), 96 | mockall::predicate::always(), 97 | mockall::predicate::eq(base_component.clone()), 98 | ) 99 | .times(2) 100 | .return_const(Ok(wire_vec.clone())); 101 | 102 | let fetch_day_day = super::FetchByDayDay {}; 103 | let result = fetch_day_day.fetch_entries_from_day_to_day::( 104 | &mock_fetchers, 105 | start_time, 106 | end_time, 107 | base_component.clone(), 108 | ); 109 | assert_eq!(result, Ok(wire_vec2.clone())); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_entries_from_day_to_hour.rs: -------------------------------------------------------------------------------- 1 | use super::fetchers::Fetchers; 2 | use crate::datetime_queries::inputs::FetchEntriesTime; 3 | use crate::wire_record::WireRecord; 4 | use chrono::{Datelike, Duration, Timelike}; 5 | use hdk::prelude::*; 6 | 7 | #[cfg(feature = "mock")] 8 | use ::mockall::automock; 9 | 10 | pub struct FetchByDayHour {} 11 | #[cfg_attr(feature = "mock", automock)] 12 | impl FetchByDayHour { 13 | /// fetches all entries of a certain type between two days where the hour is not given for the start day 14 | pub fn fetch_entries_from_day_to_hour< 15 | EntryType: 'static + TryFrom, 16 | TY, 17 | E, 18 | >( 19 | &self, 20 | fetchers: &Fetchers, 21 | link_type_filter: LinkTypeFilter, 22 | link_type: TY, 23 | start: FetchEntriesTime, 24 | end: FetchEntriesTime, 25 | base_component: String, 26 | ) -> Result>, WasmError> 27 | where 28 | ScopedLinkType: TryFrom, 29 | TY: Clone, 30 | WasmError: From, 31 | { 32 | let mut dt = start.to_date_time(); 33 | let mut entries = Vec::new(); 34 | let end = end.to_date_time(); 35 | let end_prev = end - Duration::days(1); // this is to prevent fetch entries by day being called on the last day (we don't want all the hours on the last day) 36 | while dt < end_prev { 37 | entries.push(fetchers.day.fetch_entries_by_day::( 38 | &fetchers.hour, 39 | &fetchers.get_latest, 40 | link_type_filter.clone(), 41 | link_type.clone(), 42 | FetchEntriesTime::from_date_time(dt.clone()), 43 | base_component.clone(), 44 | )); 45 | dt = dt + Duration::days(1); 46 | } 47 | while dt <= end { 48 | entries.push(fetchers.hour.fetch_entries_by_hour::( 49 | &fetchers.get_latest, 50 | link_type_filter.clone(), 51 | link_type.clone(), 52 | dt.year(), 53 | dt.month(), 54 | dt.day(), 55 | dt.hour(), 56 | base_component.clone(), 57 | )); 58 | dt = dt + Duration::hours(1); 59 | } 60 | Ok(entries 61 | .into_iter() 62 | .filter_map(Result::ok) 63 | .flatten() 64 | .collect()) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use crate::crud::example::Example; 71 | use crate::datetime_queries::fetchers::Fetchers; 72 | use crate::datetime_queries::inputs::FetchEntriesTime; 73 | 74 | use crate::wire_record::WireRecord; 75 | use ::fixt::prelude::*; 76 | use hdk::prelude::*; 77 | 78 | #[test] 79 | fn test_fetch_entries_from_day_to_hour() { 80 | // case 1: fetch by day is called once, and fetch by hour is called 3 times 81 | // another case to try out would be same start and end day, so only fetch by hour is called 82 | let start_time = FetchEntriesTime { 83 | year: 2021, 84 | month: 10 as u32, 85 | day: 20 as u32, 86 | hour: None, 87 | }; 88 | let end_time = FetchEntriesTime { 89 | year: 2021, 90 | month: 10 as u32, 91 | day: 21 as u32, 92 | hour: Some(2 as u32), 93 | }; 94 | let base_component = "create".to_string(); 95 | let wire_record = WireRecord:: { 96 | action_hash: fixt![ActionHashB64], 97 | entry_hash: fixt![EntryHashB64], 98 | entry: Example { number: 1 }, 99 | created_at: fixt![Timestamp], 100 | updated_at: fixt![Timestamp], 101 | }; 102 | let wire_vec: Vec> = vec![wire_record.clone()]; 103 | let wire_vec4 = vec![ 104 | wire_record.clone(), 105 | wire_record.clone(), 106 | wire_record.clone(), 107 | wire_record.clone(), 108 | ]; 109 | 110 | let mut mock_fetchers = Fetchers::default(); 111 | // fetch_entries_by_day should be called for each day in the range 112 | mock_fetchers 113 | .day 114 | .expect_fetch_entries_by_day::() 115 | .with( 116 | mockall::predicate::always(), 117 | mockall::predicate::always(), 118 | mockall::predicate::always(), 119 | mockall::predicate::eq(base_component.clone()), 120 | ) 121 | .times(1) 122 | .return_const(Ok(wire_vec.clone())); 123 | 124 | mock_fetchers 125 | .hour 126 | .expect_fetch_entries_by_hour::() 127 | .with( 128 | mockall::predicate::always(), 129 | mockall::predicate::eq(start_time.year), 130 | mockall::predicate::eq(start_time.month), 131 | mockall::predicate::eq(end_time.day), 132 | mockall::predicate::always(), 133 | mockall::predicate::eq(base_component.clone()), 134 | ) 135 | .times(3) 136 | .return_const(Ok(wire_vec.clone())); 137 | let fetch_day_hour = super::FetchByDayHour {}; 138 | let result = fetch_day_hour.fetch_entries_from_day_to_hour::( 139 | &mock_fetchers, 140 | start_time.clone(), 141 | end_time.clone(), 142 | base_component.clone(), 143 | ); 144 | assert_eq!(result, Ok(wire_vec4.clone())); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_entries_from_hour_to_day.rs: -------------------------------------------------------------------------------- 1 | use super::fetchers::Fetchers; 2 | use super::inputs::FetchEntriesTime; 3 | use super::utils::next_day; 4 | use crate::wire_record::WireRecord; 5 | use chrono::{Datelike, Duration, Timelike}; 6 | use hdk::prelude::*; 7 | 8 | #[cfg(feature = "mock")] 9 | use ::mockall::automock; 10 | 11 | pub struct FetchByHourDay {} 12 | #[cfg_attr(feature = "mock", automock)] 13 | impl FetchByHourDay { 14 | /// fetches all entries of a certain type between two days where the hour is not given for the end day 15 | pub fn fetch_entries_from_hour_to_day< 16 | EntryType: 'static + TryFrom, 17 | TY, 18 | E, 19 | >( 20 | &self, 21 | fetchers: &Fetchers, 22 | link_type_filter: LinkTypeFilter, 23 | link_type: TY, 24 | start: FetchEntriesTime, 25 | end: FetchEntriesTime, 26 | base_component: String, 27 | ) -> Result>, WasmError> 28 | where 29 | ScopedLinkType: TryFrom, 30 | TY: Clone, 31 | WasmError: From, 32 | { 33 | let mut dt = start.to_date_time(); 34 | let mut entries = Vec::new(); 35 | let end = end.to_date_time(); 36 | let second_day = next_day(dt.clone()); 37 | while dt < second_day { 38 | entries.push(fetchers.hour.fetch_entries_by_hour::( 39 | &fetchers.get_latest, 40 | link_type_filter.clone(), 41 | link_type.clone(), 42 | dt.year(), 43 | dt.month(), 44 | dt.day(), 45 | dt.hour(), 46 | base_component.clone(), 47 | )); 48 | dt = dt + Duration::hours(1); 49 | } 50 | while dt <= end { 51 | entries.push(fetchers.day.fetch_entries_by_day::( 52 | &fetchers.hour, 53 | &fetchers.get_latest, 54 | link_type_filter.clone(), 55 | link_type.clone(), 56 | FetchEntriesTime::from_date_time(dt.clone()), 57 | base_component.clone(), 58 | )); 59 | dt = dt + Duration::days(1); 60 | } 61 | Ok(entries 62 | .into_iter() 63 | .filter_map(Result::ok) 64 | .flatten() 65 | .collect()) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use crate::crud::example::Example; 72 | use crate::datetime_queries::fetchers::Fetchers; 73 | use crate::datetime_queries::inputs::FetchEntriesTime; 74 | 75 | use crate::wire_record::WireRecord; 76 | use ::fixt::prelude::*; 77 | use hdk::prelude::*; 78 | #[test] 79 | fn test_fetch_entries_from_day_to_hour() { 80 | // should be called for 22, 23, then twice for next two days 81 | let start_time = FetchEntriesTime { 82 | year: 2021, 83 | month: 10 as u32, 84 | day: 20 as u32, 85 | hour: Some(22 as u32), 86 | }; 87 | let end_time = FetchEntriesTime { 88 | year: 2021, 89 | month: 10 as u32, 90 | day: 22 as u32, 91 | hour: None, 92 | }; 93 | let base_component = "create".to_string(); 94 | let wire_record = WireRecord:: { 95 | action_hash: fixt![ActionHashB64], 96 | entry_hash: fixt![EntryHashB64], 97 | entry: Example { number: 1 }, 98 | created_at: fixt![Timestamp], 99 | updated_at: fixt![Timestamp], 100 | }; 101 | let wire_vec: Vec> = vec![wire_record.clone()]; 102 | let wire_vec4 = vec![ 103 | wire_record.clone(), 104 | wire_record.clone(), 105 | wire_record.clone(), 106 | wire_record.clone(), 107 | ]; 108 | 109 | let mut mock_fetchers = Fetchers::default(); 110 | // could set up the expectation each time it is expected to be called to be able to check that the inputs are as expected 111 | mock_fetchers 112 | .day 113 | .expect_fetch_entries_by_day::() 114 | .with( 115 | mockall::predicate::always(), 116 | mockall::predicate::always(), 117 | mockall::predicate::always(), 118 | mockall::predicate::eq(base_component.clone()), 119 | ) 120 | .times(2) 121 | .return_const(Ok(wire_vec.clone())); 122 | 123 | mock_fetchers 124 | .hour 125 | .expect_fetch_entries_by_hour::() 126 | .with( 127 | mockall::predicate::always(), 128 | mockall::predicate::eq(start_time.year), 129 | mockall::predicate::eq(start_time.month), 130 | mockall::predicate::eq(start_time.day), 131 | mockall::predicate::always(), 132 | mockall::predicate::eq(base_component.clone()), 133 | ) 134 | .times(2) 135 | .return_const(Ok(wire_vec.clone())); 136 | let fetch_hour_day = super::FetchByHourDay {}; 137 | let result = fetch_hour_day.fetch_entries_from_hour_to_day::( 138 | &mock_fetchers, 139 | start_time.clone(), 140 | end_time.clone(), 141 | base_component.clone(), 142 | ); 143 | assert_eq!(result, Ok(wire_vec4.clone())); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_entries_from_hour_to_hour.rs: -------------------------------------------------------------------------------- 1 | use super::fetchers::Fetchers; 2 | use super::inputs::FetchEntriesTime; 3 | use super::utils::next_day; 4 | use crate::wire_record::WireRecord; 5 | use chrono::{Datelike, Duration, Timelike}; 6 | use hdk::prelude::*; 7 | 8 | #[cfg(feature = "mock")] 9 | use ::mockall::automock; 10 | 11 | pub struct FetchByHourHour {} 12 | #[cfg_attr(feature = "mock", automock)] 13 | impl FetchByHourHour { 14 | /// fetches all entries of a certain type between two dates (day and hour) 15 | pub fn fetch_entries_from_hour_to_hour< 16 | EntryType: 'static + TryFrom, 17 | TY, 18 | E, 19 | >( 20 | &self, 21 | fetchers: &Fetchers, 22 | link_type_filter: LinkTypeFilter, 23 | link_type: TY, 24 | start: FetchEntriesTime, 25 | end: FetchEntriesTime, 26 | base_component: String, 27 | ) -> Result>, WasmError> 28 | where 29 | ScopedLinkType: TryFrom, 30 | TY: Clone, 31 | WasmError: From, 32 | { 33 | let mut dt = start.to_date_time(); 34 | let mut entries = Vec::new(); 35 | let end = end.to_date_time(); 36 | let second_day = next_day(dt.clone()); 37 | let second_last_day = end.clone() - Duration::days(1); 38 | 39 | // if hour range is on same day, skip first two loops 40 | match next_day(dt.clone()) == next_day(end.clone()) { 41 | true => {} 42 | false => { 43 | while dt < second_day { 44 | entries.push(fetchers.hour.fetch_entries_by_hour::( 45 | &fetchers.get_latest, 46 | link_type_filter.clone(), 47 | link_type.clone(), 48 | dt.year(), 49 | dt.month(), 50 | dt.day(), 51 | dt.hour(), 52 | base_component.clone(), 53 | )); 54 | dt = dt + Duration::hours(1); 55 | } 56 | while dt <= second_last_day { 57 | entries.push(fetchers.day.fetch_entries_by_day::( 58 | &fetchers.hour, 59 | &fetchers.get_latest, 60 | link_type_filter.clone(), 61 | link_type.clone(), 62 | FetchEntriesTime::from_date_time(dt.clone()), 63 | base_component.clone(), 64 | )); 65 | dt = dt + Duration::days(1); 66 | } 67 | } 68 | } 69 | while dt <= end { 70 | entries.push(fetchers.hour.fetch_entries_by_hour::( 71 | &fetchers.get_latest, 72 | link_type_filter.clone(), 73 | link_type.clone(), 74 | dt.year(), 75 | dt.month(), 76 | dt.day(), 77 | dt.hour(), 78 | base_component.clone(), 79 | )); 80 | dt = dt + Duration::hours(1); 81 | } 82 | Ok(entries 83 | .into_iter() 84 | .filter_map(Result::ok) 85 | .flatten() 86 | .collect()) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use crate::crud::example::Example; 93 | use crate::datetime_queries::fetchers::Fetchers; 94 | use crate::datetime_queries::inputs::FetchEntriesTime; 95 | 96 | use crate::wire_record::WireRecord; 97 | use ::fixt::prelude::*; 98 | use hdk::prelude::*; 99 | #[test] 100 | fn test_fetch_entries_from_day_to_hour() { 101 | // cover at least three main cases: when start and end are on the same day 102 | // when end is on the next day 103 | // when there are multiple days between start and end (will call fetch by day) 104 | 105 | // ============================================ 106 | // case 1: start and end on same day 107 | // ============================================ 108 | // expect fetch by hour to be called 4 times 109 | let start_time = FetchEntriesTime { 110 | year: 2021, 111 | month: 10 as u32, 112 | day: 20 as u32, 113 | hour: Some(2 as u32), 114 | }; 115 | let end_time = FetchEntriesTime { 116 | year: 2021, 117 | month: 10 as u32, 118 | day: 20 as u32, 119 | hour: Some(5 as u32), 120 | }; 121 | let base_component = "create".to_string(); 122 | let wire_record = WireRecord:: { 123 | action_hash: fixt![ActionHashB64], 124 | entry_hash: fixt![EntryHashB64], 125 | entry: Example { number: 1 }, 126 | created_at: fixt![Timestamp], 127 | updated_at: fixt![Timestamp], 128 | }; 129 | let wire_vec: Vec> = vec![wire_record.clone()]; 130 | let wire_vec4 = vec![ 131 | wire_record.clone(), 132 | wire_record.clone(), 133 | wire_record.clone(), 134 | wire_record.clone(), 135 | ]; 136 | 137 | let mut mock_fetchers = Fetchers::default(); 138 | mock_fetchers 139 | .hour 140 | .expect_fetch_entries_by_hour::() 141 | .with( 142 | mockall::predicate::always(), 143 | mockall::predicate::eq(start_time.year), 144 | mockall::predicate::eq(start_time.month), 145 | mockall::predicate::eq(start_time.day), 146 | mockall::predicate::always(), 147 | mockall::predicate::eq(base_component.clone()), 148 | ) 149 | .times(4) 150 | .return_const(Ok(wire_vec.clone())); 151 | let fetch_hour_hour = super::FetchByHourHour {}; 152 | let result = fetch_hour_hour.fetch_entries_from_hour_to_hour::( 153 | &mock_fetchers, 154 | start_time.clone(), 155 | end_time.clone(), 156 | base_component.clone(), 157 | ); 158 | assert_eq!(result, Ok(wire_vec4.clone())); 159 | // ============================================ 160 | // case 2: end on next day 161 | // ============================================ 162 | // expect fetch by hour to be called 4 times, fetch by day to be called 0 times 163 | let start_time = FetchEntriesTime { 164 | year: 2021, 165 | month: 10 as u32, 166 | day: 20 as u32, 167 | hour: Some(22 as u32), 168 | }; 169 | let end_time = FetchEntriesTime { 170 | year: 2021, 171 | month: 10 as u32, 172 | day: 21 as u32, 173 | hour: Some(1 as u32), 174 | }; 175 | let base_component = "create".to_string(); 176 | let wire_record = WireRecord:: { 177 | action_hash: fixt![ActionHashB64], 178 | entry_hash: fixt![EntryHashB64], 179 | entry: Example { number: 1 }, 180 | created_at: fixt![Timestamp], 181 | updated_at: fixt![Timestamp], 182 | }; 183 | let wire_vec: Vec> = vec![wire_record.clone()]; 184 | let wire_vec4 = vec![ 185 | wire_record.clone(), 186 | wire_record.clone(), 187 | wire_record.clone(), 188 | wire_record.clone(), 189 | ]; 190 | 191 | let mut mock_fetchers = Fetchers::default(); 192 | mock_fetchers 193 | .hour 194 | .expect_fetch_entries_by_hour::() 195 | .with( 196 | mockall::predicate::always(), 197 | mockall::predicate::eq(start_time.year), 198 | mockall::predicate::eq(start_time.month), 199 | mockall::predicate::always(), 200 | mockall::predicate::always(), 201 | mockall::predicate::eq(base_component.clone()), 202 | ) 203 | .times(4) 204 | .return_const(Ok(wire_vec.clone())); 205 | let fetch_hour_hour = super::FetchByHourHour {}; 206 | let result = fetch_hour_hour.fetch_entries_from_hour_to_hour::( 207 | &mock_fetchers, 208 | start_time.clone(), 209 | end_time.clone(), 210 | base_component.clone(), 211 | ); 212 | assert_eq!(result, Ok(wire_vec4.clone())); 213 | // ============================================ 214 | // case 3: multiple days between start and end 215 | // ============================================ 216 | // expect fetch by hour to be called 3 times, fetch by day to be called 4 times 217 | let start_time = FetchEntriesTime { 218 | year: 2021, 219 | month: 10 as u32, 220 | day: 20 as u32, 221 | hour: Some(23 as u32), 222 | }; 223 | let end_time = FetchEntriesTime { 224 | year: 2021, 225 | month: 10 as u32, 226 | day: 22 as u32, 227 | hour: Some(1 as u32), 228 | }; 229 | let base_component = "create".to_string(); 230 | let wire_record = WireRecord:: { 231 | action_hash: fixt![ActionHashB64], 232 | entry_hash: fixt![EntryHashB64], 233 | entry: Example { number: 1 }, 234 | created_at: fixt![Timestamp], 235 | updated_at: fixt![Timestamp], 236 | }; 237 | let wire_vec: Vec> = vec![wire_record.clone()]; 238 | let wire_vec4 = vec![ 239 | wire_record.clone(), 240 | wire_record.clone(), 241 | wire_record.clone(), 242 | wire_record.clone(), 243 | ]; 244 | 245 | let mut mock_fetchers = Fetchers::default(); 246 | // could instead put in the expected FetchEntriesTime struct 247 | mock_fetchers 248 | .day 249 | .expect_fetch_entries_by_day::() 250 | .with( 251 | mockall::predicate::always(), 252 | mockall::predicate::always(), 253 | mockall::predicate::always(), 254 | mockall::predicate::eq(base_component.clone()), 255 | ) 256 | .times(1) 257 | .return_const(Ok(wire_vec.clone())); 258 | 259 | mock_fetchers 260 | .hour 261 | .expect_fetch_entries_by_hour::() 262 | .with( 263 | mockall::predicate::always(), 264 | mockall::predicate::eq(start_time.year), 265 | mockall::predicate::eq(start_time.month), 266 | mockall::predicate::always(), 267 | mockall::predicate::always(), 268 | mockall::predicate::eq(base_component.clone()), 269 | ) 270 | .times(3) 271 | .return_const(Ok(wire_vec.clone())); 272 | let fetch_hour_hour = super::FetchByHourHour {}; 273 | let result = fetch_hour_hour.fetch_entries_from_hour_to_hour::( 274 | &mock_fetchers, 275 | start_time.clone(), 276 | end_time.clone(), 277 | base_component.clone(), 278 | ); 279 | assert_eq!(result, Ok(wire_vec4.clone())); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/datetime_queries/fetch_in_time_range.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | 3 | use super::fetchers::Fetchers; 4 | use super::inputs::FetchEntriesTime; 5 | use super::utils::is_valid_date_range; 6 | use crate::wire_record::WireRecord; 7 | 8 | /// fetches all entries of a certain type between two dates. Calls different sub methods depending on if an hour is suppled. 9 | pub fn fetch_entries_in_time_range< 10 | EntryType: 'static + TryFrom, 11 | TY, 12 | E, 13 | >( 14 | fetchers: &Fetchers, 15 | link_type_filter: LinkTypeFilter, 16 | link_type: TY, 17 | start_time: FetchEntriesTime, 18 | end_time: FetchEntriesTime, 19 | base_component: String, 20 | ) -> Result>, WasmError> 21 | where 22 | ScopedLinkType: TryFrom, 23 | TY: Clone, 24 | WasmError: From, 25 | { 26 | is_valid_date_range(start_time.clone(), end_time.clone())?; 27 | match start_time.hour { 28 | None => { 29 | match end_time.hour { 30 | None => fetchers 31 | .day_to_day 32 | .fetch_entries_from_day_to_day::( 33 | fetchers, 34 | link_type_filter, 35 | link_type, 36 | start_time.clone(), 37 | end_time.clone(), 38 | base_component, 39 | ), 40 | Some(_) => { 41 | //day to hour: loop from 1st day to 2nd last day, then loop through hours in last day 42 | fetchers 43 | .day_to_hour 44 | .fetch_entries_from_day_to_hour::( 45 | fetchers, 46 | link_type_filter, 47 | link_type, 48 | start_time.clone(), 49 | end_time.clone(), 50 | base_component, 51 | ) 52 | } 53 | } 54 | } 55 | Some(_) => { 56 | match end_time.hour { 57 | None => { 58 | // hour to day: loop through hours on first day, then 2nd day to last day 59 | fetchers 60 | .hour_to_day 61 | .fetch_entries_from_hour_to_day::( 62 | fetchers, 63 | link_type_filter, 64 | link_type, 65 | start_time.clone(), 66 | end_time.clone(), 67 | base_component, 68 | ) 69 | } 70 | Some(_) => { 71 | // hour to hour: loop through hours on first day, then 2nd day to 2nd last day, then hours on last day 72 | fetchers 73 | .hour_to_hour 74 | .fetch_entries_from_hour_to_hour::( 75 | fetchers, 76 | link_type_filter, 77 | link_type, 78 | start_time.clone(), 79 | end_time.clone(), 80 | base_component, 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use crate::crud::example::Example; 91 | use crate::datetime_queries::fetchers::Fetchers; 92 | use crate::datetime_queries::inputs::FetchEntriesTime; 93 | 94 | use crate::wire_record::WireRecord; 95 | use ::fixt::prelude::*; 96 | use hdk::prelude::*; 97 | 98 | #[test] 99 | fn test_fetch_in_time_range() { 100 | // ================================================== 101 | // day to day test 102 | // ================================================== 103 | // create inputs and outputs 104 | let start_time = FetchEntriesTime { 105 | year: 2021, 106 | month: 10 as u32, 107 | day: 20 as u32, 108 | hour: None, 109 | }; 110 | let end_time = FetchEntriesTime { 111 | year: 2021, 112 | month: 10 as u32, 113 | day: 21 as u32, 114 | hour: None, 115 | }; 116 | let base_component = "create".to_string(); 117 | let wire_record = WireRecord:: { 118 | action_hash: fixt![ActionHashB64], 119 | entry_hash: fixt![EntryHashB64], 120 | entry: Example { number: 1 }, 121 | created_at: fixt![Timestamp], 122 | updated_at: fixt![Timestamp], 123 | }; 124 | let wire_vec: Vec> = vec![wire_record]; 125 | let mut mock_fetchers = Fetchers::default(); 126 | mock_fetchers 127 | .day_to_day 128 | .expect_fetch_entries_from_day_to_day::() 129 | .with( 130 | mockall::predicate::always(), 131 | mockall::predicate::eq(start_time.clone()), 132 | mockall::predicate::eq(end_time.clone()), 133 | mockall::predicate::eq(base_component.clone()), 134 | ) 135 | .times(1) 136 | .return_const(Ok(wire_vec.clone())); 137 | 138 | let result = super::fetch_entries_in_time_range::( 139 | &mock_fetchers, 140 | start_time, 141 | end_time, 142 | base_component.clone(), 143 | ); 144 | assert_eq!(result, Ok(wire_vec.clone())); 145 | // ================================================== 146 | // day to hour test 147 | // ================================================== 148 | 149 | // create inputs and outputs 150 | let start_time = FetchEntriesTime { 151 | year: 2021, 152 | month: 10 as u32, 153 | day: 20 as u32, 154 | hour: None, 155 | }; 156 | let end_time = FetchEntriesTime { 157 | year: 2021, 158 | month: 10 as u32, 159 | day: 21 as u32, 160 | hour: Some(10 as u32), 161 | }; 162 | let mut mock_fetchers = Fetchers::default(); 163 | mock_fetchers 164 | .day_to_hour 165 | .expect_fetch_entries_from_day_to_hour::() 166 | .with( 167 | mockall::predicate::always(), 168 | mockall::predicate::eq(start_time.clone()), 169 | mockall::predicate::eq(end_time.clone()), 170 | mockall::predicate::eq(base_component.clone()), 171 | ) 172 | .times(1) 173 | .return_const(Ok(wire_vec.clone())); 174 | 175 | let result = super::fetch_entries_in_time_range::( 176 | &mock_fetchers, 177 | start_time, 178 | end_time, 179 | base_component.clone(), 180 | ); 181 | assert_eq!(result, Ok(wire_vec.clone())); 182 | // ================================================== 183 | // hour to day test 184 | // ================================================== 185 | 186 | // create inputs and outputs 187 | let start_time = FetchEntriesTime { 188 | year: 2021, 189 | month: 10 as u32, 190 | day: 20 as u32, 191 | hour: Some(10 as u32), 192 | }; 193 | let end_time = FetchEntriesTime { 194 | year: 2021, 195 | month: 10 as u32, 196 | day: 21 as u32, 197 | hour: None, 198 | }; 199 | let mut mock_fetchers = Fetchers::default(); 200 | mock_fetchers 201 | .hour_to_day 202 | .expect_fetch_entries_from_hour_to_day::() 203 | .with( 204 | mockall::predicate::always(), 205 | mockall::predicate::eq(start_time.clone()), 206 | mockall::predicate::eq(end_time.clone()), 207 | mockall::predicate::eq(base_component.clone()), 208 | ) 209 | .times(1) 210 | .return_const(Ok(wire_vec.clone())); 211 | 212 | let result = super::fetch_entries_in_time_range::( 213 | &mock_fetchers, 214 | start_time, 215 | end_time, 216 | base_component.clone(), 217 | ); 218 | assert_eq!(result, Ok(wire_vec.clone())); 219 | // ================================================== 220 | // hour to day test 221 | // ================================================== 222 | 223 | // create inputs and outputs 224 | let start_time = FetchEntriesTime { 225 | year: 2021, 226 | month: 10 as u32, 227 | day: 20 as u32, 228 | hour: Some(10 as u32), 229 | }; 230 | let end_time = FetchEntriesTime { 231 | year: 2021, 232 | month: 10 as u32, 233 | day: 21 as u32, 234 | hour: Some(10 as u32), 235 | }; 236 | let mut mock_fetchers = Fetchers::default(); 237 | mock_fetchers 238 | .hour_to_hour 239 | .expect_fetch_entries_from_hour_to_hour::() 240 | .with( 241 | mockall::predicate::always(), 242 | mockall::predicate::eq(start_time.clone()), 243 | mockall::predicate::eq(end_time.clone()), 244 | mockall::predicate::eq(base_component.clone()), 245 | ) 246 | .times(1) 247 | .return_const(Ok(wire_vec.clone())); 248 | 249 | let result = super::fetch_entries_in_time_range::( 250 | &mock_fetchers, 251 | start_time, 252 | end_time, 253 | base_component.clone(), 254 | ); 255 | assert_eq!(result, Ok(wire_vec.clone())); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/datetime_queries/fetchers.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "mock"))] 2 | use super::{ 3 | fetch_by_day::FetchByDay, fetch_by_hour::FetchByHour, 4 | fetch_entries_from_day_to_day::FetchByDayDay, fetch_entries_from_day_to_hour::FetchByDayHour, 5 | fetch_entries_from_hour_to_day::FetchByHourDay, 6 | fetch_entries_from_hour_to_hour::FetchByHourHour, 7 | }; 8 | #[cfg(not(feature = "mock"))] 9 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 10 | 11 | #[cfg(feature = "mock")] 12 | use super::{ 13 | fetch_by_day::MockFetchByDay as FetchByDay, fetch_by_hour::MockFetchByHour as FetchByHour, 14 | fetch_entries_from_day_to_day::MockFetchByDayDay as FetchByDayDay, 15 | fetch_entries_from_day_to_hour::MockFetchByDayHour as FetchByDayHour, 16 | fetch_entries_from_hour_to_day::MockFetchByHourDay as FetchByHourDay, 17 | fetch_entries_from_hour_to_hour::MockFetchByHourHour as FetchByHourHour, 18 | }; 19 | 20 | /// A struct containing all structs which implement fetching related methods. 21 | /// This way only an instance of this struct needs to be passed in to any one fetching method/function. 22 | #[cfg(feature = "mock")] 23 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 24 | pub struct Fetchers { 25 | pub day_to_day: FetchByDayDay, 26 | pub day_to_hour: FetchByDayHour, 27 | pub hour_to_day: FetchByHourDay, 28 | pub hour_to_hour: FetchByHourHour, 29 | pub day: FetchByDay, 30 | pub hour: FetchByHour, 31 | pub get_latest: GetLatestEntry, 32 | } 33 | impl Fetchers { 34 | pub fn new( 35 | day_to_day: FetchByDayDay, 36 | day_to_hour: FetchByDayHour, 37 | hour_to_day: FetchByHourDay, 38 | hour_to_hour: FetchByHourHour, 39 | day: FetchByDay, 40 | hour: FetchByHour, 41 | get_latest: GetLatestEntry, 42 | ) -> Self { 43 | Self { 44 | day_to_day, 45 | day_to_hour, 46 | hour_to_day, 47 | hour_to_hour, 48 | day, 49 | hour, 50 | get_latest, 51 | } 52 | } 53 | } 54 | #[cfg(not(feature = "mock"))] 55 | impl Fetchers { 56 | pub fn default() -> Self { 57 | Self { 58 | day_to_day: FetchByDayDay {}, 59 | day_to_hour: FetchByDayHour {}, 60 | hour_to_day: FetchByHourDay {}, 61 | hour_to_hour: FetchByHourHour {}, 62 | day: FetchByDay {}, 63 | hour: FetchByHour {}, 64 | get_latest: GetLatestEntry {}, 65 | } 66 | } 67 | } 68 | #[cfg(feature = "mock")] 69 | impl Fetchers { 70 | pub fn default() -> Self { 71 | Self { 72 | day_to_day: FetchByDayDay::new(), 73 | day_to_hour: FetchByDayHour::new(), 74 | hour_to_day: FetchByHourDay::new(), 75 | hour_to_hour: FetchByHourHour::new(), 76 | day: FetchByDay::new(), 77 | hour: FetchByHour::new(), 78 | get_latest: GetLatestEntry::new(), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/datetime_queries/inputs.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Datelike, NaiveDate, Timelike, Utc}; 2 | use hdk::prelude::*; 3 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 4 | pub struct FetchEntriesTime { 5 | pub year: i32, 6 | pub month: u32, 7 | pub day: u32, 8 | pub hour: Option, 9 | } 10 | 11 | impl FetchEntriesTime { 12 | pub fn to_date_time(&self) -> DateTime { 13 | match self.hour { 14 | None => DateTime::from_utc( 15 | NaiveDate::from_ymd(self.year, self.month, self.day).and_hms(0, 0, 0), 16 | Utc, 17 | ), 18 | Some(h) => DateTime::from_utc( 19 | NaiveDate::from_ymd(self.year, self.month, self.day).and_hms(h, 0, 0), 20 | Utc, 21 | ), 22 | } 23 | } 24 | pub fn from_date_time(dt: DateTime) -> Self { 25 | Self { 26 | year: dt.year(), 27 | month: dt.month(), 28 | day: dt.day(), 29 | hour: Some(dt.hour()), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/datetime_queries/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fetch_by_day; 2 | pub mod fetch_by_hour; 3 | pub mod fetch_by_time; 4 | pub mod fetch_entries_from_day_to_day; 5 | pub mod fetch_entries_from_day_to_hour; 6 | pub mod fetch_entries_from_hour_to_day; 7 | pub mod fetch_entries_from_hour_to_hour; 8 | pub mod fetch_in_time_range; 9 | pub mod fetchers; 10 | pub mod inputs; 11 | pub mod utils; 12 | -------------------------------------------------------------------------------- /src/datetime_queries/utils.rs: -------------------------------------------------------------------------------- 1 | use super::inputs::FetchEntriesTime; 2 | use chrono::{DateTime, Datelike, Duration, NaiveDate, Utc}; 3 | use hdk::prelude::*; 4 | 5 | pub fn is_valid_date_range( 6 | start: FetchEntriesTime, 7 | end: FetchEntriesTime, 8 | ) -> Result<(), WasmError> { 9 | match start.to_date_time() < end.to_date_time() { 10 | // Here is where we could allow for start and end to be equal 11 | true => Ok(()), 12 | false => Err(err("invalid date range")), 13 | } 14 | } 15 | pub fn next_day(date_time: DateTime) -> DateTime { 16 | let next_day = date_time + Duration::days(1); 17 | DateTime::from_utc( 18 | NaiveDate::from_ymd(next_day.year(), next_day.month(), next_day.day()).and_hms(0, 0, 0), 19 | Utc, 20 | ) 21 | } 22 | 23 | pub fn err(reason: &str) -> WasmError { 24 | wasm_error!(WasmErrorInner::Guest(String::from(reason))) 25 | } 26 | pub fn serialize_err(sbe: SerializedBytesError) -> WasmError { 27 | wasm_error!(WasmErrorInner::Serialize(sbe)) 28 | } 29 | 30 | /// used to convert the last component of a path (in this case, the hour of a day) into a string 31 | pub fn get_last_component_string(path_tag: LinkTag) -> ExternResult { 32 | let component_bytes = &path_tag.0[1..]; 33 | let component: Component = SerializedBytes::from(UnsafeBytes::from(component_bytes.to_vec())) 34 | .try_into() 35 | .map_err(serialize_err)?; 36 | let hour_str: String = String::try_from(&component).map_err(serialize_err)?; 37 | Ok(hour_str) 38 | } 39 | 40 | pub fn day_path_from_date( 41 | link_type: TY, 42 | base_component: String, 43 | year: i32, 44 | month: u32, 45 | day: u32, 46 | ) -> ExternResult 47 | where 48 | ScopedLinkType: TryFrom, 49 | WasmError: From, 50 | { 51 | Path::from(format!("{}.{}-{}-{}", base_component, year, month, day)).typed(link_type) 52 | } 53 | 54 | pub fn hour_path_from_date( 55 | link_type: TY, 56 | base_component: String, 57 | year: i32, 58 | month: u32, 59 | day: u32, 60 | hour: u32, 61 | ) -> ExternResult 62 | where 63 | ScopedLinkType: TryFrom, 64 | WasmError: From, 65 | { 66 | Path::from(format!( 67 | "{}.{}-{}-{}.{}", 68 | base_component, year, month, day, hour 69 | )) 70 | .typed(link_type) 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod modify_chain; 2 | pub mod crud; 3 | pub mod datetime_queries; 4 | pub mod retrieval; 5 | pub mod signals; 6 | pub mod wire_record; 7 | -------------------------------------------------------------------------------- /src/modify_chain/do_create.rs: -------------------------------------------------------------------------------- 1 | use crate::wire_record::WireRecord; 2 | use crate::{datetime_queries::utils::serialize_err, modify_chain::utils::add_current_time_path}; 3 | use hdk::prelude::*; 4 | use holo_hash::{ActionHashB64, AgentPubKey, EntryHashB64}; 5 | 6 | #[cfg(feature = "mock")] 7 | use ::mockall::automock; 8 | 9 | use super::utils::create_link_relaxed; 10 | 11 | /// an enum passed into do_create to indicate whether the newly created entry is to be 12 | /// linked off a path (like an anchor for entry types) or a supplied entry hash 13 | #[derive(Debug, PartialEq, Clone)] 14 | pub enum TypedPathOrEntryHash { 15 | TypedPath(TypedPath), 16 | EntryHash(EntryHash), 17 | } 18 | 19 | /// a struct which implements a [do_create](DoCreate::do_create) method 20 | /// a method is used instead of a function so that it can be mocked to simplify unit testing 21 | #[derive(Debug, PartialEq, Clone)] 22 | pub struct DoCreate {} 23 | #[cfg_attr(feature = "mock", automock)] 24 | impl DoCreate { 25 | /// This will create an entry and will either link it off the main Path or a supplied entry hash. 26 | /// It can also optionally send a signal of this event to all peers supplied in `send_signal_to_peers` 27 | /// uses `ChainTopOrdering::Relaxed` such that multiple creates can be committed in parallel 28 | pub fn do_create( 29 | &self, 30 | full_entry: MyEntryTypes, 31 | inner_entry: CrudType, 32 | link_off: Option, 33 | entry_type_id: String, 34 | scoped_link_type: R, 35 | send_signal_to_peers: Option>, 36 | add_time_path: Option, 37 | ) -> ExternResult> 38 | where 39 | CrudType: Clone, 40 | ScopedEntryDefIndex: for<'a> TryFrom<&'a MyEntryTypes, Error = E>, 41 | EntryVisibility: for<'a> From<&'a MyEntryTypes>, 42 | Entry: 'static + TryFrom, 43 | ScopedLinkType: TryFrom, 44 | R: Clone, 45 | WasmError: From, 46 | MyEntryTypes: 'static + Clone, 47 | AppEntryBytes: TryFrom, 48 | S: 'static 49 | + From> 50 | + serde::Serialize 51 | + std::fmt::Debug, 52 | E: 'static, 53 | { 54 | // calling create instead of create_entry to be able to indicate relaxed chain ordering 55 | let ScopedEntryDefIndex { 56 | zome_index, 57 | zome_type: entry_def_index, 58 | } = (&full_entry).try_into()?; 59 | let visibility = EntryVisibility::from(&full_entry); 60 | let address = create(CreateInput::new( 61 | EntryDefLocation::app(zome_index, entry_def_index), 62 | visibility, 63 | full_entry.clone().try_into()?, 64 | ChainTopOrdering::Relaxed, 65 | ))?; 66 | let entry_hash = hash_entry(full_entry.clone())?; 67 | match link_off { 68 | None => (), //no link is made 69 | Some(path_or_entry_hash) => match path_or_entry_hash { 70 | TypedPathOrEntryHash::TypedPath(path) => { 71 | // link off entry path 72 | path.ensure()?; 73 | let path_hash = path.path_entry_hash()?; 74 | create_link_relaxed( 75 | path_hash, 76 | entry_hash.clone(), 77 | scoped_link_type.clone(), 78 | LinkTag::from(vec![]), 79 | )?; 80 | } 81 | TypedPathOrEntryHash::EntryHash(base_entry_hash) => { 82 | // link off supplied entry hash 83 | create_link_relaxed( 84 | base_entry_hash, 85 | entry_hash.clone(), 86 | scoped_link_type.clone(), 87 | LinkTag::from(vec![]), 88 | )?; 89 | } 90 | }, 91 | } 92 | match add_time_path { 93 | None => (), 94 | Some(base_component) => { 95 | // create a time_path 96 | add_current_time_path( 97 | base_component, 98 | entry_hash.clone(), 99 | scoped_link_type, 100 | LinkTag::from(vec![]), 101 | )?; 102 | } 103 | } 104 | let time = sys_time()?; // this won't exactly match the timestamp stored in the record details 105 | let wire_entry: WireRecord = WireRecord { 106 | entry: inner_entry, 107 | action_hash: ActionHashB64::new(address), 108 | entry_hash: EntryHashB64::new(entry_hash), 109 | created_at: time, 110 | updated_at: time, 111 | }; 112 | 113 | match send_signal_to_peers { 114 | None => (), 115 | Some(vec_peers) => { 116 | let action_signal: crate::signals::ActionSignal = 117 | crate::signals::ActionSignal { 118 | entry_type: entry_type_id, 119 | action: crate::signals::ActionType::Create, 120 | data: crate::signals::SignalData::Create(wire_entry.clone()), 121 | }; 122 | let signal = S::from(action_signal); 123 | let payload = ExternIO::encode(signal).map_err(serialize_err)?; 124 | send_remote_signal(payload, vec_peers)?; 125 | } 126 | } 127 | Ok(wire_entry) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/modify_chain/do_delete.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | use holo_hash::{ActionHashB64, AgentPubKey}; 3 | 4 | #[cfg(feature = "mock")] 5 | use ::mockall::automock; 6 | 7 | use crate::datetime_queries::utils::serialize_err; 8 | 9 | /// a struct which implements a [do_delete](DoDelete::do_delete) method 10 | /// a method is used instead of a function so that it can be mocked to simplify unit testing 11 | #[derive(Debug, PartialEq, Clone)] 12 | pub struct DoDelete {} 13 | #[cfg_attr(feature = "mock", automock)] 14 | impl DoDelete { 15 | /// This will mark the entry at `address` as "deleted". 16 | /// It can also optionally send a signal of this event to all peers supplied in `send_signal_to_peers` 17 | /// to all peers returned by the `get_peers` call given during the macro call to `crud!` 18 | pub fn do_delete( 19 | &self, 20 | action_hash: ActionHashB64, 21 | entry_type_id: String, 22 | send_signal_to_peers: Option>, 23 | ) -> ExternResult 24 | where 25 | Entry: 'static + TryFrom, 26 | WasmError: 'static + From, 27 | T: 'static + Clone, 28 | AppEntryBytes: 'static + TryFrom, 29 | S: 'static + From> + serde::Serialize + std::fmt::Debug, 30 | E: 'static, 31 | { 32 | delete_entry(DeleteInput::new( 33 | action_hash.clone().into(), 34 | ChainTopOrdering::Relaxed, 35 | ))?; 36 | match send_signal_to_peers { 37 | None => (), 38 | Some(vec_peers) => { 39 | let action_signal: crate::signals::ActionSignal = crate::signals::ActionSignal { 40 | entry_type: entry_type_id, 41 | action: crate::signals::ActionType::Delete, 42 | data: crate::signals::SignalData::Delete::(action_hash.clone()), 43 | }; 44 | let signal = S::from(action_signal); 45 | let payload = ExternIO::encode(signal).map_err(serialize_err)?; 46 | send_remote_signal(payload, vec_peers)?; 47 | } 48 | } 49 | Ok(action_hash) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modify_chain/do_fetch.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "mock"))] 2 | use crate::retrieval::fetch_entries::FetchEntries; 3 | #[cfg(not(feature = "mock"))] 4 | use crate::retrieval::fetch_links::FetchLinks; 5 | #[cfg(not(feature = "mock"))] 6 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 7 | 8 | #[cfg(feature = "mock")] 9 | use crate::retrieval::fetch_entries::MockFetchEntries as FetchEntries; 10 | #[cfg(feature = "mock")] 11 | use crate::retrieval::fetch_links::MockFetchLinks as FetchLinks; 12 | #[cfg(feature = "mock")] 13 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 14 | 15 | use crate::wire_record::WireRecord; 16 | use hdi::hash_path::path::TypedPath; 17 | use hdk::prelude::*; 18 | 19 | #[cfg(feature = "mock")] 20 | use ::mockall::automock; 21 | 22 | /// a struct which implements a [do_fetch](DoFetch::do_fetch) method 23 | /// a method is used instead of a function so that it can be mocked to simplify unit testing 24 | #[derive(Debug, PartialEq, Clone)] 25 | pub struct DoFetch {} 26 | #[cfg_attr(feature = "mock", automock)] 27 | impl DoFetch { 28 | /// This is the exposed/public Zome function for either fetching ALL or a SPECIFIC list of the entries of the type. 29 | pub fn do_fetch( 30 | &self, 31 | fetch_entries: &FetchEntries, 32 | fetch_links: &FetchLinks, 33 | get_latest: &GetLatestEntry, 34 | fetch_options: crate::retrieval::inputs::FetchOptions, 35 | get_options: GetOptions, 36 | link_type: LinkTypeFilter, 37 | link_tag: Option, 38 | path: TypedPath, 39 | ) -> ExternResult>> 40 | where 41 | Entry: TryFrom, 42 | WasmError: From, 43 | T: 'static + Clone + TryFrom, 44 | E: 'static, 45 | { 46 | let entries = fetch_entries.fetch_entries::( 47 | fetch_links, 48 | &get_latest, 49 | link_type, 50 | link_tag, 51 | path, 52 | fetch_options, 53 | get_options, 54 | )?; 55 | Ok(entries) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/modify_chain/do_update.rs: -------------------------------------------------------------------------------- 1 | use crate::wire_record::WireRecord; 2 | use crate::{datetime_queries::utils::serialize_err, modify_chain::utils::add_current_time_path}; 3 | use hdk::prelude::*; 4 | use holo_hash::{ActionHashB64, AgentPubKey, EntryHashB64}; 5 | 6 | #[cfg(feature = "mock")] 7 | use ::mockall::automock; 8 | 9 | /// a struct which implements a [do_update](DoUpdate::do_update) method 10 | /// a method is used instead of a function so that it can be mocked to simplify unit testing 11 | #[derive(Debug, PartialEq, Clone)] 12 | pub struct DoUpdate {} 13 | #[cfg_attr(feature = "mock", automock)] 14 | impl DoUpdate { 15 | /// This will add an update to an entry. 16 | /// It can also optionally send a signal of this event to all peers supplied in `send_signal_to_peers` 17 | /// uses `ChainTopOrdering::Relaxed` such that multiple updates can be committed in parallel 18 | pub fn do_update( 19 | &self, 20 | entry: T, 21 | action_hash: ActionHashB64, 22 | entry_type_id: String, 23 | scoped_link_type: R, 24 | send_signal_to_peers: Option>, 25 | add_time_path: Option, 26 | ) -> ExternResult> 27 | where 28 | Entry: TryFrom, 29 | ScopedLinkType: TryFrom, 30 | R: Clone, 31 | WasmError: From, 32 | T: 'static + Clone, 33 | AppEntryBytes: TryFrom, 34 | S: 'static + From> + serde::Serialize + std::fmt::Debug, 35 | E: 'static, 36 | { 37 | // calling update instead of update_entry to be able to indicate relaxed chain ordering 38 | hdk::entry::update(UpdateInput { 39 | original_action_address: action_hash.clone().into(), 40 | entry: Entry::App(entry.clone().try_into()?), 41 | chain_top_ordering: ChainTopOrdering::Relaxed, 42 | })?; 43 | let entry_address = hash_entry(entry.clone())?; 44 | match add_time_path { 45 | None => (), 46 | Some(base_component) => { 47 | // create a time_path 48 | add_current_time_path( 49 | base_component, 50 | entry_address.clone(), 51 | scoped_link_type, 52 | LinkTag::from(vec![]), 53 | )?; 54 | } 55 | } 56 | let updated_at = sys_time()?; 57 | // get create time from the action_hash 58 | let maybe_record = get(ActionHash::from(action_hash.clone()), GetOptions::default())?; 59 | let created_at = match maybe_record { 60 | Some(record) => Ok(record.signed_action().action().timestamp()), 61 | None => Err(wasm_error!(WasmErrorInner::Guest(String::from( 62 | "unable to get record from provided action hash", 63 | )))), 64 | }?; 65 | let wire_entry: WireRecord = WireRecord { 66 | entry, 67 | action_hash, 68 | entry_hash: EntryHashB64::new(entry_address), 69 | created_at, 70 | updated_at, 71 | }; 72 | match send_signal_to_peers { 73 | None => (), 74 | Some(vec_peers) => { 75 | let action_signal: crate::signals::ActionSignal = crate::signals::ActionSignal { 76 | entry_type: entry_type_id, 77 | action: crate::signals::ActionType::Update, 78 | data: crate::signals::SignalData::Update(wire_entry.clone()), 79 | }; 80 | let signal = S::from(action_signal); 81 | let payload = ExternIO::encode(signal).map_err(serialize_err)?; 82 | send_remote_signal(payload, vec_peers)?; 83 | } 84 | } 85 | Ok(wire_entry) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/modify_chain/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod do_create; 2 | pub mod do_delete; 3 | pub mod do_fetch; 4 | pub mod do_update; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /src/modify_chain/utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc}; 2 | use hdk::prelude::*; 3 | use holo_hash::EntryHash; 4 | 5 | /// just like create_link but uses ChainTopOrdering::Relaxed 6 | pub fn create_link_relaxed( 7 | base_address: impl Into, 8 | target_address: impl Into, 9 | link_type: T, 10 | tag: impl Into, 11 | ) -> ExternResult 12 | where 13 | ScopedLinkType: TryFrom, 14 | WasmError: From, 15 | { 16 | let ScopedLinkType { 17 | zome_index, 18 | zome_type: link_type, 19 | } = link_type.try_into()?; 20 | HDK.with(|h| { 21 | h.borrow().create_link(CreateLinkInput::new( 22 | base_address.into(), 23 | target_address.into(), 24 | zome_index, 25 | link_type, 26 | tag.into(), 27 | ChainTopOrdering::Relaxed, 28 | )) 29 | }) 30 | } 31 | 32 | /// get the current UTC date time 33 | pub fn now_date_time() -> ExternResult<::chrono::DateTime<::chrono::Utc>> { 34 | let time = sys_time()?.as_seconds_and_nanos(); 35 | 36 | let date: DateTime = 37 | DateTime::from_utc(NaiveDateTime::from_timestamp(time.0, time.1), Utc); 38 | Ok(date) 39 | } 40 | 41 | pub fn add_current_time_path( 42 | base_component: String, 43 | entry_address: EntryHash, 44 | link_type: T, 45 | link_tag: LinkTag, 46 | ) -> ExternResult<()> 47 | where 48 | ScopedLinkType: TryFrom, 49 | T: Clone, 50 | WasmError: From, 51 | { 52 | let date: DateTime = now_date_time()?; 53 | 54 | let time_path = crate::datetime_queries::utils::hour_path_from_date( 55 | link_type.clone(), 56 | base_component, 57 | date.year(), 58 | date.month(), 59 | date.day(), 60 | date.hour(), 61 | )?; 62 | 63 | time_path.ensure()?; 64 | create_link( 65 | time_path.path_entry_hash()?, 66 | entry_address.clone(), 67 | link_type, 68 | link_tag, 69 | )?; 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/retrieval/fetch_entries.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "mock"))] 2 | use crate::retrieval::fetch_links::FetchLinks; 3 | #[cfg(not(feature = "mock"))] 4 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 5 | 6 | #[cfg(feature = "mock")] 7 | use crate::retrieval::fetch_links::MockFetchLinks as FetchLinks; 8 | #[cfg(feature = "mock")] 9 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 10 | 11 | #[cfg(feature = "mock")] 12 | use ::mockall::automock; 13 | 14 | use crate::retrieval::inputs::FetchOptions; 15 | use crate::wire_record::WireRecord; 16 | use hdk::prelude::*; 17 | use std::convert::identity; 18 | 19 | #[derive(Debug, PartialEq, Clone)] 20 | pub struct FetchEntries {} 21 | #[cfg_attr(feature = "mock", automock)] 22 | impl FetchEntries { 23 | // TODO: change this in such a way that the path is only passed in if it is needed (for fetching all), for example `All(String)` pass in the path as string 24 | /// Fetch either all entries of a certain type (assuming they are linked to a path) or a specific subset given their entry hashes. 25 | pub fn fetch_entries< 26 | EntryType: 'static + TryFrom, 27 | >( 28 | &self, 29 | fetch_links: &FetchLinks, 30 | get_latest: &GetLatestEntry, 31 | link_type: LinkTypeFilter, 32 | link_tag: Option, 33 | entry_path: TypedPath, // TODO: see if there is a way to derive this from the entry itself (like from entry id) 34 | fetch_options: FetchOptions, 35 | get_options: GetOptions, 36 | ) -> Result>, WasmError> { 37 | match fetch_options { 38 | FetchOptions::All => { 39 | let path_hash = entry_path.path_entry_hash()?; 40 | fetch_links.fetch_links::( 41 | get_latest, 42 | path_hash, 43 | link_type, 44 | link_tag, 45 | get_options, 46 | ) 47 | // TODO: will have to instantiate or pass in the struct 48 | } 49 | FetchOptions::Specific(vec_entry_hash) => { 50 | let entries = vec_entry_hash 51 | .iter() 52 | .map(|entry_hash| { 53 | get_latest.get_latest_for_entry::( 54 | EntryHash::from(entry_hash.clone()).into(), 55 | get_options.clone(), 56 | ) 57 | }) 58 | // drop Err(_) and unwraps Ok(_) 59 | .filter_map(Result::ok) 60 | // drop None and unwraps Some(_) 61 | .filter_map(identity) 62 | .map(|x| WireRecord::from(x)) 63 | .collect(); 64 | Ok(entries) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/retrieval/fetch_links.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "mock"))] 2 | use crate::retrieval::get_latest_for_entry::GetLatestEntry; 3 | #[cfg(feature = "mock")] 4 | use crate::retrieval::get_latest_for_entry::MockGetLatestEntry as GetLatestEntry; 5 | 6 | use crate::wire_record::WireRecord; 7 | use hdk::prelude::*; 8 | use std::convert::identity; 9 | 10 | #[cfg(feature = "mock")] 11 | use ::mockall::automock; 12 | 13 | #[derive(Debug, PartialEq, Clone)] 14 | pub struct FetchLinks {} 15 | #[cfg_attr(feature = "mock", automock)] 16 | impl FetchLinks { 17 | /// Fetch and deserialize all the entries of a certain type that are linked to an EntryHash. 18 | /// Useful for having a Path that you link everything to. This also internally calls [get_latest_for_entry](super::get_latest_for_entry::GetLatestEntry::get_latest_for_entry) meaning 19 | /// that the contents for each entry returned are automatically the latest contents. 20 | pub fn fetch_links< 21 | EntryType: 'static + TryFrom, 22 | >( 23 | &self, 24 | get_latest: &GetLatestEntry, 25 | entry_hash: EntryHash, 26 | link_type: LinkTypeFilter, 27 | link_tag: Option, 28 | get_options: GetOptions, 29 | ) -> Result>, WasmError> { 30 | let mut input = GetLinksInputBuilder::try_new(entry_hash, link_type)?; 31 | if let Some(link_tag_inner) = link_tag { 32 | input = input.tag_prefix(link_tag_inner); 33 | } 34 | Ok(get_links(input.build())? 35 | .into_iter() 36 | .map(|link: Link| { 37 | get_latest.get_latest_for_entry::( 38 | link.target.try_into().map_err(|_| { 39 | wasm_error!(WasmErrorInner::Guest("Target is not an entry".to_string())) 40 | })?, 41 | get_options.clone(), 42 | ) 43 | }) 44 | .filter_map(Result::ok) 45 | .filter_map(identity) 46 | .map(|x| WireRecord::from(x)) 47 | .collect()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/retrieval/get_latest_for_entry.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | 3 | use crate::{datetime_queries::utils::serialize_err, retrieval::utils::*, wire_record::WireRecord}; 4 | 5 | #[cfg(feature = "mock")] 6 | use ::mockall::automock; 7 | 8 | #[derive(Debug, PartialEq, Clone)] 9 | pub struct GetLatestEntry {} 10 | #[cfg_attr(feature = "mock", automock)] 11 | impl GetLatestEntry { 12 | /// If an entry at the `entry_hash` has multiple updates to itself, this 13 | /// function will sort through them by timestamp in order to return the contents 14 | /// of the latest update. It also has the special behaviour of returning the 15 | /// ORIGINAL ActionHash, as opposed to the ActionHash of the Action that performed 16 | /// that latest update. This is useful if you want hashes in your application 17 | /// to act consistently, almost acting as an "id" in a centralized system. 18 | /// It simplifies traversal of the update tree, since all updates 19 | /// made by the client can reference the original, instead of updates reference updates 20 | pub fn get_latest_for_entry< 21 | T: 'static + TryFrom, 22 | >( 23 | &self, 24 | entry_hash: EntryHash, 25 | get_options: GetOptions, 26 | ) -> ExternResult>> { 27 | match get_details(entry_hash.clone(), get_options.clone())? { 28 | Some(Details::Entry(details)) => match details.entry_dht_status { 29 | EntryDhtStatus::Live => { 30 | let first_action = details.actions.first().unwrap(); 31 | let created_at = first_action.action().timestamp(); 32 | match details.updates.len() { 33 | // pass out the action associated with this entry 34 | 0 => { 35 | let updated_at = created_at.clone(); 36 | let maybe_entry_and_hashes = original_action_hash_with_entry( 37 | get_action_hash(first_action.to_owned()), 38 | get_options, 39 | )?; 40 | match maybe_entry_and_hashes { 41 | Some(entry_and_hashes) => Ok(Some(WireRecord { 42 | action_hash: entry_and_hashes.1.into(), 43 | entry_hash: entry_and_hashes.2.into(), 44 | entry: entry_and_hashes.0, 45 | created_at, 46 | updated_at, 47 | })), 48 | None => Ok(None), 49 | } 50 | } 51 | _ => { 52 | let mut sortlist = details.updates.to_vec(); 53 | // unix timestamp should work for sorting 54 | sortlist.sort_by_key(|update| update.action().timestamp().as_millis()); 55 | // sorts in ascending order, so take the last record 56 | let last = sortlist.last().unwrap().to_owned(); 57 | let updated_at = last.action().timestamp(); 58 | let maybe_entry_and_hashes = original_action_hash_with_entry( 59 | get_action_hash(last), 60 | get_options, 61 | )?; 62 | match maybe_entry_and_hashes { 63 | Some(entry_and_hashes) => Ok(Some(WireRecord { 64 | action_hash: entry_and_hashes.1.into(), 65 | entry_hash: entry_and_hashes.2.into(), 66 | entry: entry_and_hashes.0, 67 | created_at, 68 | updated_at, 69 | })), 70 | None => Ok(None), 71 | } 72 | } 73 | } 74 | } 75 | EntryDhtStatus::Dead => Ok(None), 76 | _ => Ok(None), 77 | }, 78 | _ => Ok(None), 79 | } 80 | } 81 | } 82 | 83 | fn original_action_hash_with_entry< 84 | T: 'static + TryFrom, 85 | >( 86 | action_hash: ActionHash, 87 | get_options: GetOptions, 88 | ) -> ExternResult> { 89 | match get(action_hash, get_options)? { 90 | Some(record) => match record.entry().to_app_option::().map_err(serialize_err)? { 91 | Some(entry) => Ok(Some(( 92 | entry, 93 | match record.action() { 94 | // we DO want to return the action for the original 95 | // instead of the updated, in our case 96 | Action::Update(update) => update.original_action_address.clone(), 97 | Action::Create(_) => record.action_address().clone(), 98 | _ => { 99 | unreachable!("Can't have returned a action for a nonexistent entry") 100 | } 101 | }, 102 | record.action().entry_hash().unwrap().to_owned(), 103 | ))), 104 | None => Ok(None), 105 | }, 106 | None => Ok(None), 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/retrieval/inputs.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | use holo_hash::EntryHashB64; 3 | 4 | #[derive(Debug, PartialEq, Serialize, Deserialize, SerializedBytes)] 5 | pub enum FetchOptions { 6 | All, 7 | Specific(Vec), 8 | } 9 | -------------------------------------------------------------------------------- /src/retrieval/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fetch_entries; 2 | pub mod fetch_links; 3 | pub mod get_latest_for_entry; 4 | pub mod inputs; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /src/retrieval/utils.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | 3 | /// convert a SignedActionHashed which are like raw contents 4 | /// into the ActionHash of itself 5 | pub fn get_action_hash(signed_action_hashed: SignedActionHashed) -> ActionHash { 6 | signed_action_hashed.as_hash().to_owned() 7 | } 8 | -------------------------------------------------------------------------------- /src/signals.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | use holo_hash::ActionHashB64; 3 | use std::fmt; 4 | 5 | use crate::wire_record::WireRecord; 6 | 7 | /// when sending signals, distinguish 8 | /// between "create", "update", and "delete" actions 9 | /// via this enum. Serializes to/from "create" | "update" | "delete" 10 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, SerializedBytes)] 11 | #[serde(from = "UIEnum")] 12 | #[serde(into = "UIEnum")] 13 | pub enum ActionType { 14 | Create, 15 | Update, 16 | Delete, 17 | } 18 | 19 | #[derive(Debug, Serialize, Deserialize, SerializedBytes, Clone, PartialEq)] 20 | struct UIEnum(String); 21 | 22 | impl From for ActionType { 23 | fn from(ui_enum: UIEnum) -> Self { 24 | match ui_enum.0.as_str() { 25 | "create" => Self::Create, 26 | "update" => Self::Update, 27 | _ => Self::Delete, 28 | } 29 | } 30 | } 31 | 32 | impl From for UIEnum { 33 | fn from(action_type: ActionType) -> Self { 34 | Self(action_type.to_string().to_lowercase()) 35 | } 36 | } 37 | 38 | impl fmt::Display for ActionType { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | write!(f, "{:?}", self) 41 | } 42 | } 43 | 44 | /// Grant unrestricted access for this agent to receive 45 | /// calls to its `recv_remote_signal` endpoint via others 46 | /// calling `remote_signal` 47 | pub fn create_receive_signal_cap_grant() -> ExternResult<()> { 48 | let mut functions = BTreeSet::new(); 49 | functions.insert((zome_info()?.name, "recv_remote_signal".into())); 50 | 51 | create_cap_grant(CapGrantEntry { 52 | tag: "".into(), 53 | // empty access converts to unrestricted 54 | access: ().into(), 55 | functions: GrantedFunctions::Listed(functions), 56 | })?; 57 | Ok(()) 58 | } 59 | 60 | /// Distinguishes between what data structures should be passed 61 | /// to the UI based on different action types, like create/update/delete 62 | /// this will be used to send these data structures as signals to the UI 63 | /// When Create/Update, we will pass the actual new Entry 64 | /// but when doing Delete we will naturally only pass the ActionHash 65 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 66 | // untagged because the useful tagging is done externally on the *Signal object 67 | // as the tag and action 68 | #[serde(untagged)] 69 | pub enum SignalData { 70 | Create(WireRecord), 71 | Update(WireRecord), 72 | Delete(ActionHashB64), 73 | } 74 | 75 | /// This will be used to send data events as signals to the UI. All 76 | /// signals relating to the entry type will share this high level structure, creating consistency. 77 | /// The `data` field should use the variant (Create/Update/Delete) 78 | /// that matches the variant for `action`. So if `action` is variant [ActionType::Create](crate::signals::ActionType::Create) 79 | #[doc = " then `data` should be `SignalData::Create`."] 80 | /// It serializes with camelCase style replacement of underscores in object keys. 81 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] 82 | #[serde(rename_all = "camelCase")] 83 | pub struct ActionSignal { 84 | pub entry_type: String, 85 | pub action: ActionType, 86 | pub data: SignalData, 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::create_receive_signal_cap_grant; 92 | use ::fixt::prelude::*; 93 | use hdk::prelude::*; 94 | 95 | #[test] 96 | fn test_create_receive_signal_cap_grant() { 97 | // set up the mock hdk responses 98 | let mut mock_hdk = MockHdkT::new(); 99 | // zome info is dynamic so 100 | // that this is generic and usable in any zome 101 | let zome_info = fixt!(ZomeInfo); 102 | mock_hdk 103 | .expect_zome_info() 104 | .times(1) 105 | .return_const(Ok(zome_info.clone())); 106 | // create_cap_grant calls just `create` under the hood 107 | let mut functions: GrantedFunctions = BTreeSet::new(); 108 | functions.insert((zome_info.name, "recv_remote_signal".into())); 109 | let expected = CreateInput::new( 110 | EntryDefId::CapGrant, 111 | EntryVisibility::Private, 112 | Entry::CapGrant(CapGrantEntry { 113 | tag: "".into(), 114 | // empty access converts to unrestricted 115 | access: ().into(), 116 | functions, 117 | }), 118 | ChainTopOrdering::Strict, 119 | ); 120 | let action_hash = fixt!(ActionHash); 121 | mock_hdk 122 | .expect_create() 123 | .with(mockall::predicate::eq(expected)) 124 | .times(1) 125 | .return_const(Ok(action_hash)); 126 | set_hdk(mock_hdk); 127 | // call the function we are testing 128 | let result = create_receive_signal_cap_grant(); 129 | assert_eq!(result.is_ok(), true); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/wire_record.rs: -------------------------------------------------------------------------------- 1 | use hdk::prelude::*; 2 | use holo_hash::{EntryHashB64, ActionHashB64}; 3 | 4 | #[doc = "This data structure will be very broadly useful and represents 5 | how an entry should be serialized along with what metadata to 6 | form a consistent pattern that the UI or client can expect. 7 | It is called `WireRecord` because it is how data looks passed 8 | 'over the wire' or network."] 9 | /// It serializes with camelCase style replacement of underscores in object keys. 10 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct WireRecord { 13 | pub action_hash: ActionHashB64, 14 | pub entry_hash: EntryHashB64, 15 | pub entry: T, 16 | pub created_at: Timestamp, 17 | pub updated_at: Timestamp, 18 | } --------------------------------------------------------------------------------