Entries

An entry is a blob of bytes that your application code gives meaning and structure to via serialization, deserialization, and validation.

Entries and actions

An entry is always paired with an entry creation action that tells you who authored it and when it was authored. Because of this, you don’t usually need to include author and timestamp fields in your entries. There are two kinds of entry creation action:

The pairing of an entry and the action that created it is called a record, which is the basic unit of data in a Holochain application.

Define an entry type

Each entry has a type, which your application code uses to make sense of the entry’s bytes. Our HDI library gives you macros to automatically define, serialize, and deserialize entry types to and from any Rust struct or enum that serde can handle.

Entry types are defined in an integrity zome. To define an EntryType, use the hdi::prelude::hdk_entry_helper macro on your Rust type:

use hdi::prelude::*;

#[hdk_entry_helper]
pub struct Movie {
  title: String,
  director: String,
  imdb_id: Option<String>,
  release_date: Timestamp,
  box_office_revenue: u128,
}

This implements a host of TryFrom conversions that your type is expected to implement, along with serialization and deserialization functions.

In order to dispatch validation to the proper integrity zome, Holochain needs to know about all the entry types that your integrity zome defines. This is done by implementing a callback in your zome called entry_defs, but it’s easier to use the hdi::prelude::hdk_entry_defs macro on an enum of all the entry types:

use hdi::prelude::*;

#[hdk_entry_defs]
enum EntryTypes {
  Movie(Movie),
  // other types...
}

Each variant in the enum should hold the Rust type that corresponds to it, and is implicitly marked with an entry_def proc macro which, if you specify it explicitly, lets you configure the given entry type further:

use hdi::prelude::*;

#[hdk_entry_defs]
enum EntryTypes {
    #[entry_def(required_validations = 7, )]
    Movie(Movie),

    // You can reuse your Rust type in another entry type if you like. In this
    // example, `HomeMovie` also (de)serializes to/from the `Movie` struct, but
    // is actually a different entry type with different visibility, and can be
    // subjected to different validation rules.
    #[entry_def(visibility = "private", )]
    HomeMovie(Movie),
}

This also gives you an enum that you can use later when you’re storing app data. This is important because, under the hood, an entry type consists of two bytes – an integrity zome index and an entry def index. These are required whenever you want to write an entry. Instead of having to remember those values every time you store something, your coordinator zome can just import and use this enum, which already knows how to convert each entry type to the right IDs.

Create an entry

Most of the time you’ll want to define your create, read, update, and delete (CRUD) functions in a coordinator zome rather than the integrity zome that defines it. This is because a coordinator zome is easier to update in the wild than an integrity zome.

Create an entry by calling hdk::prelude::create_entry. If you used hdk_entry_helper and hdk_entry_defs macro in your integrity zome (see Define an entry type), you can use the entry types enum you defined, and the entry will be serialized and have the correct integrity zome and entry type indexes added to it.

use hdk::prelude::*;
use chrono::Date;
// Import the entry types and the enum defined in the integrity zome.
use movie_integrity::*;

let movie = Movie {
  title: "The Good, the Bad, and the Ugly",
  director: "Sergio Leone"
  imdb_id: Some("tt0060196"),
  release_date: Timestamp::from(Date::Utc("1966-12-23")),
  box_office_revenue: 389_000_000,
};

let create_action_hash: ActionHash = create_entry(
    // The value you pass to `create_entry` needs a lot of traits to tell
    // Holochain which entry type from which integrity zome you're trying to
    // create. The `hdk_entry_defs` macro will have set this up for you, so all
    // you need to do is wrap your movie in the corresponding enum variant.
    &EntryTypes::Movie(movie.clone()),
)?;

Create under the hood

When the client calls a zome function that calls create_entry, Holochain does the following:

  1. Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
  2. Build an entry creation action called Create that includes:
    • the author’s public key,
    • a timestamp,
    • the action’s sequence in the source chain and the previous action’s hash,
    • the entry type (integrity zome index and entry type index), and
    • the hash of the serialized entry data.
  3. Write the Create action and the serialized entry data to the scratch space.
  4. Return the ActionHash of the Create action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.)
  5. Wait for the zome function to complete.
  6. Convert the action to DHT operations.
  7. Run the validation callback for all DHT operations.
    • If successful, continue.
    • If unsuccessful, return the validation error to the client instead of the zome function’s return value.
  8. Compare the scratch space against the actual state of the source chain.
    • If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a HeadMoved error is returned to the caller.
    • If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
    • If the source chain has not diverged, the data in the scratch space is written to the source chain state.
  9. Return the zome function’s return value to the client.
  10. In the background, publish all newly created DHT operations to their respective authority agents.

Update an Entry

Update an entry creation action by calling hdk::prelude::update_entry with the old action hash and the new entry data:

use hdk::prelude::*;
use chrono::Date;
use movie_integrity::*;

let movie2 = Movie {
  title: "The Good, the Bad, and the Ugly",
  director: "Sergio Leone"
  imdb_id: Some("tt0060196"),
  release_date: Timestamp::from(Date::Utc("1966-12-23")),
  box_office_revenue: 400_000_000,
};

let update_action_hash: ActionHash = update_entry(
    create_action_hash,
    &EntryTypes::Movie(movie2.clone()),
)?;

An Update action operates on an entry creation action (either a Create or an Update), not just an entry by itself. It also doesn’t remove the original data from the DHT; instead, it gets attached to both the original entry and its entry creation action. As an entry creation action itself, it references the hash of the new entry so it can be retrieved from the DHT.

Update under the hood

Calling update_entry does the following:

  1. Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
  2. Build an Update action that contains everything in a Create action, plus:
    • the hash of the original action and
    • the hash of the original action’s serialized entry data. (Note that the entry type is automatically retrieved from the original action.)
  3. Write an Update action to the scratch space.
  4. Return the ActionHash of the Update action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.)
  5. Wait for the zome function to complete.
  6. Convert the action to DHT operations.
  7. Run the validation callback for all DHT operations.
    • If successful, continue.
    • If unsuccessful, return the validation error to the client instead of the zome function’s return value.
  8. Compare the scratch space against the actual state of the source chain.
    • If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a HeadMoved error is returned to the caller.
    • If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
    • If the source chain has not diverged, the data in the scratch space is written to the source chain state.
  9. Return the zome function’s return value to the client.
  10. In the background, publish all newly created DHT operations to their respective authority agents.

Update patterns

Holochain gives you this update_entry function, but is somewhat unopinionated about how it’s used. While in most cases you’ll want to interpret it as applying to the original record (action + entry), there are cases where you might want to interpret it as applying to the original entry, because the Update action is merely a piece of metadata attached to both, and can be retrieved along with the original data using hdk::prelude::get_details (see below).

You can also choose what record updates should be attached to. You can structure them as a ‘list’, where all updates refer to the ActionHash of the original Create action.

Update 1
Create
Update 2
Update 3
Update 4

Or you can structure your updates as a ‘chain’, where each update refers to the ActionHash of the previous entry creation action (either an Update or the original Create).

Update 1
Create
Update 2
Update 3
Update 4

If you structure your updates as a chain, you may want to also create links from the ActionHash of the original Create to each update in the chain, making it a hybrid of a list and a chain. This trades additional storage space for reduced lookup time.

Resolving update conflicts

It’s up to you to decide whether two updates on the same entry or action are a conflict. If your use case allows branching edits similar to Git, then conflicts aren’t an issue.

But if your use case needs a single canonical version of a resource, you’ll need to decide on a conflict resolution strategy to use at retrieval time.

If only the original author is permitted to update the entry, choosing the latest update is simple. Just choose the Update action with the most recent timestamp, which is guaranteed to advance monotonically for any honest agent’s source chain. But if multiple agents are permitted to update an entry, it gets more complicated. Two agents could make an update at exactly the same time (or their action timestamps might be wrong or falsified). So, how do you decide which is the ‘latest’ update?

These are two common patterns:

Delete an entry

Delete an entry creation action by calling hdk::prelude::delete_entry.

use hdk::prelude::*;

let delete_action_hash: ActionHash = delete_entry(
    create_action_hash,
)?;

As with an update, this does not actually remove data from the source chain or the DHT. Instead, a Delete action is authored, which attaches to the entry creation action and marks it as ‘dead’. An entry itself is only considered dead when all entry creation actions that created it are marked dead, and it can become live again in the future if a new entry creation action writes it. Dead data can still be retrieved with hdk::prelude::get_details (see below).

In the future we plan to include a ‘purge’ functionality. This will give agents permission to actually erase an entry from their DHT store, but not its associated entry creation action.

Remember that, even once purge is implemented, it is impossible to force another person to delete data once they have seen it. Be deliberate about choosing what data becomes public in your app.

Delete under the hood

Calling delete_entry does the following:

  1. Prepare a scratch space for making an atomic set of changes to the source chain for the agent’s cell.
  2. Write a Delete action to the scratch space.
  3. Return the ActionHash of the Delete action to the calling zome function. (At this point, the action hasn’t been persisted to the source chain.)
  4. Wait for the zome function to complete.
  5. Convert the action to DHT operations.
  6. Run the validation callback for all DHT operations.
    • If successful, continue.
    • If unsuccessful, return the validation error to the client instead of the zome function’s return value.
  7. Compare the scratch space against the actual state of the source chain.
    • If the source chain has diverged from the scratch space, and the write specified strict chain top ordering, the scratch space is discarded and a HeadMoved error is returned to the caller.
    • If the source chain has diverged and the write specified relaxed chain top ordering, the data in the scratch space is ‘rebased’ on top of the new source chain state as it’s being written.
    • If the source chain has not diverged, the data in the scratch space is written to the source chain state.
  8. Return the zome function’s return value to the client.
  9. In the background, publish all newly created DHT operations to their respective authority agents.

Identifiers on the DHT

Holochain uses the hash of a piece of content as its unique ID. In practice, different kinds of hashes have different meaning and suitability to use as an identifier:

You can use any of these identifiers as a field in your entry types to model a many-to-one relationship, or you can use links between identifiers to model a one-to-many relationship.

Retrieve an entry

By record only

Get a record by calling hdk::prelude::get with the hash of its entry creation action. The return value is a Result<holochain_integrity_types::record::Record>.

You can also pass an entry hash to get, and the record returned will contain the oldest live entry creation action that wrote it.

use hdk::prelude::*;
use movie_integrity::*;

let maybe_record: Option<Record> = get(
    action_hash,
    // Get the data and metadata directly from the DHT. You can also specify
    // `GetOptions::content()`, which only accesses the DHT if the data at the
    // supplied hash doesn't already exist locally.
    GetOptions::latest()
)?;

match maybe_record {
    Some(record) => {
        // Not all types of action contain entry data, and if they do, it may
        // not be accessible, so `.entry()` may return nothing. It may also be
        // of an unexpected entry type, so it may not be deserializable to an
        // instance of the expected Rust type. You can find out how to check for
        // most of these edge cases by exploring the documentation for the
        // `Record` type, but in this simple example we'll skip that and assume
        // that the action hash does reference an action with entry data
        // attached to it.
        let maybe_movie: Option<Movie> = record.entry().to_app_option();

        match maybe_movie {
            Some(movie) => debug!(
                "Movie {}, released {}, record stored by {} on {}",
                movie.title,
                movie.release_date,
                record.action().author(),
                record.action().timestamp()
            ),
            None => debug!("Movie entry couldn't be retrieved"),
        }
    }
    None => debug!("Movie record not found"),
}

All records and links attached to an entry

To get a record and all the updates, deletes, and outbound links associated with its action, as well as its current validation status, call hdk::prelude::get_details with an action hash. You’ll receive a Result<holochain_zome_types::metadata::RecordDetails>.

use hdk::prelude::*;
use movie_integrity::*;

let maybe_details: Option<Details> = get_details(
    action_hash,
    GetOptions::latest()
)?;

match maybe_details {
    Some(Details::Record(record_details)) => {
        let maybe_movie: Option<Movie> = record.entry().to_app_option();
        match maybe_movie {
            Some(movie) => debug!(
                "Movie record {}, created on {}, was updated by {} agents and deleted by {} agents",
                movie.title,
                record_details.record.action().timestamp(),
                record_details.updates.len(),
                record_details.deletes.len()
            ),
            None => debug!("Movie entry couldn't be retrieved"),
        }
    }
    _ => debug!("Movie record not found"),
}

To get an entry and all the deletes and updates that operated on it (or rather, that operated on the entry creation actions that produced it), as well as all its entry creation actions and its current status on the DHT, pass an entry hash to hdk::prelude::get_details. You’ll receive a holochain_zome_types::metadata::EntryDetails struct.

use hdk::prelude::*;
use movie_integrity::*;

let maybe_details: Option<Details> = get_details(
    entry_hash,
    GetOptions::latest()
)?;

match maybe_details {
    Some(Details::Entry(entry_details)) => {
        let maybe_movie: Option<Movie> = entry_details.entry
            .as_app_entry()
            .clone()
            .try_into()
            .ok();
        match maybe_movie {
            Some(movie) => debug!(
                "Movie {} was written by {} agents, updated by {} agents, and deleted by {} agents. Its DHT status is currently {}.",
                movie.title,
                entry_details.actions.len(),
                entry_details.updates.len(),
                entry_details.deletes.len(),
                entry_details.entry_dht_status
            ),
            None => debug!("Movie entry couldn't be retrieved"),
        }
    }
    _ => debug!("Movie entry not found"),
}

Scaffolding an entry type and CRUD API

The Holochain dev tool command hc scaffold entry-type <entry_type> generates the code for a simple entry type and a CRUD API. It presents an interface that lets you define a struct and its fields, then asks you to choose whether to implement update and delete functions for it along with the default create and read functions.

Community CRUD libraries

If the scaffolder doesn’t support your desired functionality, or is too low-level, there are some community-maintained libraries that offer opinionated and high-level ways to work with entries. Some of them also offer permissions management.

Reference

Further reading

It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.