hdk/link.rs
1use crate::prelude::*;
2
3pub mod builder;
4
5pub use builder::*;
6pub use hdi::link::*;
7
8/// Create a link from a base hash to a target hash, with an optional tag.
9///
10/// Links represent the general idea of relationships between data.
11///
12/// Links are different from the tree of CRUD relationships:
13///
14/// Links:
15///
16/// - reference two hashes, base and target, and can be a local entry/action or some external hash
17/// - there is only one way to create a link, validation logic depends on only the base+target+tag
18/// - can represent circular references because only hashes are needed
19/// - support arbitrary bytes of data (i.e. "tag") that can be read or used to filter gets
20/// - deletes always point to a _specific_ link creation event, not the link itself
21/// - model dynamic sets of or relationships between things
22/// - can reference any hash regardless of type (e.g. posts can link to comments)
23///
24/// Note: There is a hard limit of 1kb of data for the tag.
25///
26/// CRUD:
27///
28/// - creates reference a single entry
29/// - updates and deletes reference create/update records by both their entry+action
30/// - creates, updates and deletes all have different functions, network ops and validation logic
31/// - is cryptographically guaranteed to be a DAG (not-circular) because they include actions
32/// - model "mutability" for a single thing/identity in an immutable/append-only way
33/// - only reference other entries of the same entry type (e.g. comments can _not_ update posts)
34///
35/// See [ `get_details` ] and get for more information about CRUD
36/// See [ `get_links` ] and [ `get_link_details` ] for more information about filtering by tag
37///
38/// Generally links and CRUDs _do not interact_ beyond the fact that links need hashes to
39/// reference for the base and target to already exist due to a prior create or update.
40/// The referenced data only needs to exist on the DHT for the link to validate, it doesn't need to be
41/// live and can have any combination of valid/invalid crud actions.
42/// i.e. if you use link_entries! to create relationships between two entries, then update_entry
43/// on the base, the links will still only be visible to get_link(s_details)! against the original
44/// base, there is no logic to "bring forward" links to the updated entry because:
45///
46/// - as per CRUD tree docs there is no "one size fits all" way to walk a tree of CRUDs
47/// - links are very generic and could even represent a comment thread against a specific revision
48/// such as those found against individual updates in a wiki/CMS tool so they need to stay where
49/// they were explicitly placed
50/// - it would actually be pretty crazy at the network layer to be gossiping links around to chase
51/// the "current revision" even if you could somehow unambiguously define "current revision"
52///
53/// This can be frustrating if you want "get all the links" for an entry but also be tracking your
54/// revision history somehow.
55/// A simple pattern to workaround this is to create an immutable (updates and deletes are invalid)
56/// "identity" entry that links reference and is referenced as a field on the entry struct of each
57/// create/update action.
58/// If you have the hash of the identity entry you can get all the links, if you have the entry or
59/// action hash for any of the creates or updates you can lookup the identity entry hash out of the
60/// body of the create/update entry.
61pub fn create_link<T, E>(
62 base_address: impl Into<AnyLinkableHash>,
63 target_address: impl Into<AnyLinkableHash>,
64 link_type: T,
65 tag: impl Into<LinkTag>,
66) -> ExternResult<ActionHash>
67where
68 ScopedLinkType: TryFrom<T, Error = E>,
69 WasmError: From<E>,
70{
71 let ScopedLinkType {
72 zome_index,
73 zome_type: link_type,
74 } = link_type.try_into()?;
75 HDK.with(|h| {
76 h.borrow().create_link(CreateLinkInput::new(
77 base_address.into(),
78 target_address.into(),
79 zome_index,
80 link_type,
81 tag.into(),
82 ChainTopOrdering::default(),
83 ))
84 })
85}
86
87/// Delete a specific link creation record.
88///
89/// Links are defined by a [OR-Set CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#OR-Set_(Observed-Remove_Set))
90/// of "Creates" and "Deletes".
91/// The deletes form a "tombstone set", each of which can nullify one of the creates.
92/// A link only "exists" if it has one or more "creates" which have not been nullified by a "delete".
93///
94/// For this reason the delete references the Create Action, not the Entry.
95/// Even more than that, both creates and deletes are _only_ actions, there is no separate entry
96/// and the delete is not simply a renamed mirror of the create (i.e. with base and target).
97///
98/// Consider what would happen if the system simply had "create link" and "delete link" pointing at
99/// the base and target without pairing:
100/// - there would be no way to revert a specific link creation
101/// - a delete may be intended for an create you haven't seen yet, so network unpredictability
102/// would cause re-ording of any view on create/deletes which means an agent can see more deletes
103/// than creates, etc.
104/// - there would only be two ways to summarise the state of a relationship between two entries,
105/// either "there are N more/less creates than deletes" or "there is at least one delete", the
106/// former leads to flakiness as above and the latter means it would be impossible to create a
107/// link after any previous delete of any link.
108///
109/// All of this is bad so link creates point to entries (See [ `create_link` ]) and deletes point to
110/// creates.
111pub fn delete_link(address: ActionHash) -> ExternResult<ActionHash> {
112 HDK.with(|h| {
113 h.borrow()
114 .delete_link(DeleteLinkInput::new(address, ChainTopOrdering::default()))
115 })
116}
117
118/// Returns all links that reference a base hash, filtered by link type and other criteria.
119/// Use a [ `GetLinksInputBuilder` ] to create the [ `GetLinksInput` ] and optionally filter links further.
120///
121/// _Note this will only get links that are defined in dependent integrity zomes._
122///
123/// Tag filtering is a simple bytes prefix.
124///
125/// e.g. if you had these links:
126/// - a: `[ 1, 2, 3]`
127/// - b: `[ 1, 2, 4]`
128/// - c: `[ 1, 3, 5]`
129///
130/// Then tag filters:
131/// - `[ 1 ]` returns `[ a, b, c]`
132/// - `[ 1, 2 ]` returns `[ a, b ]`
133/// - `[ 1, 2, 3 ]` returns `[ a ]`
134/// - `[ 5 ]` returns `[ ]` (does _not_ return c because the filter is by "prefix", not "contains")
135///
136/// This is mostly identical to [ `get_link_details` ] but returns only creates that have not been
137/// deleted, whereas `get_link_details` returns all the creates and all the deletes together.
138/// Also note that, unlike when [ `get` ] is used to retrieve an entry, links that
139/// only differ by author and creation time are not deduplicated; hence, you may receive multiple
140/// links with the same base, tag, and target.
141///
142/// See [ `get_link_details` ].
143pub fn get_links(input: GetLinksInput) -> ExternResult<Vec<Link>> {
144 Ok(HDK
145 .with(|h| h.borrow().get_links(vec![input]))?
146 .into_iter()
147 .flatten()
148 .collect())
149}
150
151/// Get all link creates and deletes that reference a base hash, optionally filtered by type or tag.
152///
153/// Type can be filtered by providing a variant of the link types, or a range of them. To get links of
154/// all types, the full range operator can be used: `get_links(base, .., None)`. Furthermore, vectors of
155/// link types can be passed in to specify multiple types. Refer to the `get_links` function in
156/// [this coordinator zome](https://github.com/holochain/holochain/blob/develop/crates/test_utils/wasm/wasm_workspace/link/src/coordinator.rs)
157/// for several examples.
158///
159/// Tag filtering is a simple bytes prefix.
160///
161/// e.g. if you had these links:
162/// - a: `[ 1, 2, 3]`
163/// - b: `[ 1, 2, 4]`
164/// - c: `[ 1, 3, 5]`
165///
166/// then tag filters:
167/// - `[ 1 ]` returns `[ a, b, c]`
168/// - `[ 1, 2 ]` returns `[ a, b ]`
169/// - `[ 1, 2, 3 ]` returns `[ a ]`
170/// - `[ 5 ]` returns `[ ]` (does _not_ return c because the filter is by "prefix", not "contains")
171///
172/// This is mostly identical to get_links but it returns all the creates and all the deletes.
173/// c.f. get_links that returns only the creates that have not been deleted.
174///
175/// See [ `get_links` ].
176pub fn get_link_details(
177 base: impl Into<AnyLinkableHash>,
178 link_type: impl LinkTypeFilterExt,
179 link_tag: Option<LinkTag>,
180 get_options: GetOptions,
181) -> ExternResult<LinkDetails> {
182 Ok(HDK
183 .with(|h| {
184 let mut input = GetLinksInputBuilder::try_new(base.into(), link_type)?;
185 if let Some(link_tag) = link_tag {
186 input = input.tag_prefix(link_tag);
187 }
188 input = input.get_options(get_options.strategy);
189 h.borrow().get_link_details(vec![input.build()])
190 })?
191 .into_iter()
192 .next()
193 .unwrap())
194}
195
196pub fn count_links(query: LinkQuery) -> ExternResult<usize> {
197 HDK.with(|h| h.borrow().count_links(query))
198}