/
record_helpers.rs
278 lines (239 loc) · 10.4 KB
/
record_helpers.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/**
* Record-handling abstractions for Holochain apps
*
* Allows for data layers which behave like a traditional graph database, where
* 'records' are the core abstraction, managed by complex arrangements of DHT
* entries and links.
*
* @package HoloREA
* @since 2019-07-02
*/
use std::convert::TryFrom;
use hdk3::prelude::*;
use crate::{
GraphAPIResult, DataIntegrityError,
record_interface::{Identifiable, Identified, Updateable},
entries::{
get_entry_by_header,
create_entry,
update_entry,
delete_entry,
},
identity_helpers::{
create_entry_identity,
read_entry_identity,
calculate_identity_address,
},
};
/// Helper to retrieve the HeaderHash for an Element
///
fn get_header_hash(shh: element::SignedHeaderHashed) -> HeaderHash {
shh.header_hashed().as_hash().to_owned()
}
//--------------------------------[ READ ]--------------------------------------
/// Read a record's entry data by its identity index
///
/// :TODO: Currently, the most recent version of the given entry will
/// be provided instead of the exact entry specified.
/// We should also check for multiple live headers, and throw a
/// conflict error if necessary. But core may implement this for
/// us eventually. (@see EntryDhtStatus)
///
pub (crate) fn read_record_entry_by_identity<T, R, O>(
identity_address: &EntryHash,
) -> GraphAPIResult<(HeaderHash, O, T)>
where O: From<EntryHash>,
SerializedBytes: TryInto<R, Error = SerializedBytesError>,
R: Identified<T>,
{
// read active links to current version
let entry_hash = read_entry_identity(identity_address)?;
// pull details of the current version, to ensure we have the most recent
let latest_header_hash = (match get_details(entry_hash, GetOptions)? {
Some(Details::Entry(details)) => match details.entry_dht_status {
metadata::EntryDhtStatus::Live => match details.updates.len() {
0 => {
// no updates yet, latest header hash is the first one
Ok(get_header_hash(details.headers.first().unwrap().to_owned()))
},
_ => {
// updates exist, find most recent header
let mut sortlist = details.updates.to_vec();
sortlist.sort_by_key(|update| update.header().timestamp().0);
let last = sortlist.last().unwrap().to_owned();
Ok(get_header_hash(last))
},
},
_ => Err(DataIntegrityError::EntryNotFound),
},
_ => Err(DataIntegrityError::EntryNotFound),
})?;
let out_header_hash = latest_header_hash.to_owned();
let storage_entry: R = get_entry_by_header(&latest_header_hash)?;
Ok((out_header_hash, storage_entry.identity()?.into(), storage_entry.entry()))
}
/// Read a record's entry data by locating it via an anchor `Path` composed
/// of some root component and (uniquely identifying) initial identity address.
///
pub fn read_record_entry<T, R, O, A, S>(
entry_type_root_path: &S,
address: &A,
) -> GraphAPIResult<(HeaderHash, O, T)>
where S: AsRef<str>,
A: AsRef<EntryHash>,
O: From<EntryHash>,
SerializedBytes: TryInto<R, Error = SerializedBytesError>,
R: Identified<T>,
{
let identity_address = calculate_identity_address(entry_type_root_path, address.as_ref())?;
read_record_entry_by_identity::<T, R, O>(&identity_address)
}
/// Fetches all referenced record entries found corresponding to the input
/// identity addresses.
///
/// Useful in loading the results of indexed data, where indexes link identity `Path`s for different records.
///
pub (crate) fn get_records_by_identity_address<'a, T, R, A>(addresses: &'a Vec<EntryHash>) -> Vec<GraphAPIResult<(HeaderHash, A, T)>>
where A: From<EntryHash>,
SerializedBytes: TryInto<R, Error = SerializedBytesError>,
R: Identified<T>,
{
addresses.iter()
.map(read_record_entry_by_identity)
.collect()
}
//-------------------------------[ CREATE ]-------------------------------------
/// Creates a new record in the DHT, assigns it an identity index (@see identity_helpers.rs)
/// and returns a tuple of this version's `HeaderHash`, the identity `EntryHash` and initial record `entry` data.
///
pub fn create_record<'a, E: 'a, R: 'a, C, A, S: AsRef<str>>(
entry_type: S,
create_payload: C,
) -> GraphAPIResult<(HeaderHash, A, E)>
where A: From<EntryHash>,
C: Into<E>,
E: Identifiable<R>,
R: Identified<E>,
EntryDefId: From<&'a R>, SerializedBytes: TryFrom<&'a R, Error = SerializedBytesError>,
{
// convert the type's CREATE payload into internal storage struct
let entry_data: E = create_payload.into();
// wrap data with null identity for origin record
let storage = entry_data.with_identity(None);
// write underlying entry
let (header_hash, entry_hash) = create_entry(&storage)?;
// create an identifier for the new entry
let base_address = create_entry_identity(entry_type, &entry_hash)?;
// link the identifier to the actual entry
create_link(base_address, entry_hash.clone(), LinkTag::new(crate::identifiers::RECORD_INITIAL_ENTRY_LINK_TAG))?;
Ok((header_hash, entry_hash.into(), entry_data))
}
//-------------------------------[ UPDATE ]-------------------------------------
/// Updates a record in the DHT by its `HeaderHash` (revision ID)
///
/// The way in which the input update payload is applied to the existing
/// entry data is up to the implementor of `Updateable<U>` for the entry type.
///
/// :TODO: prevent multiple updates to the same HeaderHash under standard operations
///
/// @see hdk_graph_helpers::record_interface::Updateable
///
pub fn update_record<'a, E: 'a, R: 'a, U, I>(
address: &'a HeaderHash,
update_payload: U,
) -> GraphAPIResult<(HeaderHash, I, E)>
where I: From<EntryHash>,
E: Identifiable<R> + Updateable<U>,
R: Identified<E>,
EntryDefId: From<&'a R>,
SerializedBytes: TryFrom<&'a R, Error = SerializedBytesError>,
SerializedBytes: TryInto<R, Error = SerializedBytesError>,
{
// get referenced entry for the given header
let previous: R = get_entry_by_header(address)?;
let prev_entry = previous.entry();
let identity_hash = previous.identity()?;
// apply update payload
let new_entry = prev_entry.update_with(update_payload);
let storage: R = new_entry.with_identity(Some(identity_hash.clone()));
// perform regular entry update using internal address
let (header_addr, _entry_addr) = update_entry(address, &storage)?;
Ok((header_addr, identity_hash.into(), new_entry))
}
//-------------------------------[ DELETE ]-------------------------------------
/// Removes a record of the given `HeaderHash` from the DHT by marking it as deleted.
///
/// Links are not affected so as to retain a link to the referencing information, which may now need to be updated.
///
pub fn delete_record<T>(address: &HeaderHash) -> GraphAPIResult<bool>
where SerializedBytes: TryInto<T, Error = SerializedBytesError>,
{
// :TODO: handle deletion of the identity `Path` for the referenced entry if this is the last header being deleted
delete_entry::<T>(address)?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{bind_identity, simple_alias};
simple_alias!(EntryId => EntryHash);
#[derive(Clone, Serialize, Deserialize, SerializedBytes, PartialEq, Debug)]
pub struct Entry {
field: Option<String>,
}
bind_identity!(Entry);
entry_def!(EntryWithIdentity EntryDef {
id: "test_entry".into(),
visibility: EntryVisibility::Public,
crdt_type: CrdtType,
required_validations: 1.into(),
required_validation_type: RequiredValidationType::default(),
});
#[derive(Clone)]
pub struct CreateRequest {
field: Option<String>,
}
impl From<CreateRequest> for Entry {
fn from(e: CreateRequest) -> Entry {
Entry {
field: e.field.into(),
}
}
}
#[derive(Clone)]
pub struct UpdateRequest {
field: Option<String>,
}
impl Updateable<UpdateRequest> for Entry {
fn update_with(&self, e: UpdateRequest) -> Entry {
Entry {
field: e.field.to_owned(),
}
}
}
#[test]
fn test_roundtrip() {
let entry_type: String = "testing".to_string();
// CREATE
let (header_addr, base_address, initial_entry): (_, EntryId, Entry) = create_record(&entry_type, CreateRequest { field: None }).unwrap();
// Verify read
let (header_addr_2, returned_address, first_entry) = read_record_entry::<Entry, EntryWithIdentity, EntryId,_,_>(&entry_type, &base_address).unwrap();
assert_eq!(header_addr, header_addr_2, "record should have same header ID on read as for creation");
assert_eq!(base_address.as_ref(), returned_address.as_ref(), "record should have same identifier ID on read as for creation");
assert_eq!(initial_entry, first_entry, "record from creation output should be same as read data");
// UPDATE
let (updated_header_addr, identity_address, updated_entry): (_, EntryId, Entry) = update_record(&header_addr, UpdateRequest { field: Some("value".into()) }).unwrap();
// Verify update & read
assert_eq!(base_address.as_ref(), identity_address.as_ref(), "record should have consistent ID over updates");
assert_ne!(header_addr, updated_header_addr, "record revision should change after update");
assert_eq!(updated_entry, Entry { field: Some("value".into()) }, "returned record should be changed after update");
let (header_addr_3, returned_address_3, third_entry) = read_record_entry::<Entry, EntryWithIdentity, EntryId,_,_>(&entry_type, &identity_address).unwrap();
assert_eq!(base_address.as_ref(), returned_address_3.as_ref(), "record should have consistent ID over updates");
assert_eq!(header_addr_3, updated_header_addr, "record revision should be same as latest update");
assert_eq!(third_entry, Entry { field: Some("value".into()) }, "retrieved record should be changed after update");
// DELETE
delete_record::<Entry>(&updated_header_addr);
// Verify read failure
let _failure = read_record_entry::<Entry, EntryWithIdentity, EntryId,_,_>(&entry_type, &identity_address).err().unwrap();
}
}