#indexed-db #idb #macro-derive #object-store #future

deli

A convenience wrapper on idb create for easily creating and managing object stores in an indexed db on browsers using derive macros

1 unstable release

0.1.0 Nov 23, 2022

#1064 in WebAssembly

MIT/Apache

54KB
1K SLoC

deli

Deli is a convenience wrapper on idb create for easily creating and managing object stores in an indexed db on browsers using derive macros.

Usage

To use deli, you need to add the following in your Cargo.toml:

[dependencies]
deli = "0.1"

deli is intended to be used on browsers using webassembly. So, make sure to compile your project with --target wasm32-unknown-unknown. Alternatively, you can add following build configuration in your .cargo/config.toml:

[build]
target = "wasm32-unknown-unknown"

Example

Defining a Model

The first step is to define your data model using Model derive macro. You also need to implement serde::Serialize and serde::Deserialize trait for your model so that the data can be converted to json before saving it into the store.

use deli::Model;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Model)]
pub struct Employee {
    #[deli(auto_increment)]
    pub id: u32,
    pub name: String,
    #[deli(unique)]
    pub email: String,
    #[deli(index)]
    pub age: u8,
}

Model derive macro automatically implements Model trait for your struct and creates a Store for accessing and writing data to the store.

Container attributes
  • #[deli(name)]: In indexed DB, by default, it creates a new object store with name of the struct (in the above example, it'll create an object store Employee in indexed db) when creating a database. To change the default object store name, use #[deli(name = "your_object_store_name")].

  • #[deli(store_name)]: By default, the derive macro will create a <ModelName>Store struct (in the above example, it'll create a EmployeeStore struct). To change the default name, use #[deli(store_name = "YourStoreName")].

Field attributes
  • #[deli(key)]: Defines the primary key path for object store.
  • #[deli(auto_increment)]: Defines the primary key path for object store with auto_increment values (implies #[deli(key)]).
  • #[deli(index)]: Creates an index for the field.
  • #[deli(unique)]: Creates an unique index for the field (implies #[deli(index)]).
  • #[deli(multi_entry)]: Creates a multi entry index for the field (implies #[deli(index)]).
  • #[deli(rename)]: Rename a field in object store. Note that this should be consistent with serde serialization. For example, if you use #[serde(rename_all = "camelCase")] you need to appropriately rename the fields for deli to be in sync with serde serialization.

Creating a Database

Next step is to create a new Database and register your models with it.

use deli::{Database, Error};

async fn create_database() -> Result<Database, Error> {
    let database = Database.builder("test_db", 1).register_model::<Employee>().await?;
}

Starting a Transaction

Once you've created a Database instance, you can start reading and writing data to database using transactions.

use deli::{Database, Error, Transaction};

fn create_read_transaction(database: &Database) -> Result<Transaction, Error> {
    database.transaction().with_model::<Employee>().build()
}

fn create_write_transaction(database: &Database) -> Result<Transaction, Error> {
    database.transaction().writable().with_model::<Employee>().build()
}

You can add multiple .with_model::<Model>() calls to add more than one model to the transaction.

Reading and writing data to a Model store

Once you have a transaction for a model, you can read or write data to that model.

use deli::{Error, Model, Transaction};

async fn add_employee(transaction: &Transaction) -> Result<u32, Error> {
    Employee::with_transaction(transaction)?.add("Alice", "alice@example.com", &25).await
}

async fn get_employee(transaction: &Transaction, id: u32) -> Result<Option<Employee>, Error> {
    Employee::with_transaction(transaction)?.get(&id).await
}

async fn get_all_employees(transaction: &Transaction) -> Result<Vec<Employee>, Error> {
    // NOTE: Here `..` (i.e., `RangeFull`) means fetch all values from store
    Employee::with_transaction(transaction)?.get_all(.., None).await
}

async fn get_employees_with_bounds(
    transaction: &Transaction,
    from_id: u32,
    to_id: u32,
) -> Result<Vec<Employee>, Error> {
    Employee::with_transaction(transaction)?.get_all(&from_id..=&to_id, None).await
}

Commiting a Transaction

After all your writes are done, you can commit the transaction:

async fn commit_transaction(transaction: Transaction) -> Result<(), Error> {
    transaction.commit().await
}

Note that commit() doesn’t normally have to be called — a transaction will automatically commit when all outstanding requests have been satisfied and no new requests have been made.

Also, be careful when using long-lived indexed db transactions as the behavior may change depending on the browser. For example, the transaction may get auto-committed when doing IO (network request) in the event loop.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~11–20MB
~282K SLoC