//// Core types and value converters for glindex.
////
//// This module exposes the fundamental building blocks used across the entire
//// library. Use `store` and `index` to declare type-safe store and index
//// definitions that bundle their name together with serialization and
//// deserialization codecs. Use `Query` to target records by key, and the value
//// helpers (`int`, `string`, `float`, `object`, …) to build the `Value`s that
//// IndexedDB operates on.
////
//// The recommended entry points for actual database work are:
////
//// - [`glindex/database`](./glindex/database.html) - open and manage databases.
//// - [`glindex/upgrade`](./glindex/upgrade.html) - schema migrations inside a version-change transaction.
//// - [`glindex/transaction`](./glindex/transaction.html) - build and start transactions over one or more object stores.
//// - [`glindex/store`](./glindex/store.html) - read and write operations on object stores within a transaction.
//// - [`glindex/index`](./glindex/index.html) - read operations on indexes within a transaction.
//// - [`glindex/cursor`](./glindex/cursor.html) - cursor-based iteration over store or index records.
import gleam/dict.{type Dict}
import gleam/dynamic/decode
import gleam/float
import gleam/int
import gleam/list
import gleam/order.{type Order}
import gleam/time/calendar.{type Date, type TimeOfDay}
import gleam/time/timestamp.{type Timestamp}
/// A definition of an IndexedDB object store, bundling its name with the
/// codecs needed to serialize records and keys to `Value` and decode them back.
///
/// Create one with `store` (inline key) or `store_with_out_of_line_key`
/// (out-of-line key), then pass it to `transaction.store` to obtain a handle
/// for use inside a transaction:
///
/// ```gleam
/// pub type TrackStore
///
/// pub fn track_store() -> Store(TrackStore, _, _, _) {
/// glindex.store(
/// name: "tracks",
/// to_value: fn(track: Track, _action) { ... },
/// decoder: track_decoder(),
/// to_key: fn(id: Int) { glindex.int(id) },
/// key_decoder: decode.int,
/// )
/// }
/// ```
///
pub opaque type Store(store_type, key_mode, t, k) {
Store(
name: String,
to_value: fn(t, Action) -> Value,
decoder: decode.Decoder(t),
to_key: fn(k) -> Value,
key_decoder: decode.Decoder(k),
)
}
@internal
pub type InlineKey
@internal
pub type OutOfLineKey
/// Create a store definition with an inline key.
///
/// Use this when the primary key is a property of the stored object (i.e. the
/// store was created with a `KeyPath`). The `to_value` function receives the
/// record and an `Action` so you can omit the key field on `Add` when
/// IndexedDB generates it automatically.
///
pub fn store(
name name: String,
to_value to_value: fn(t, Action) -> Value,
decoder decoder: decode.Decoder(t),
to_key to_key: fn(k) -> Value,
key_decoder key_decoder: decode.Decoder(k),
) -> Store(store_type, InlineKey, t, k) {
Store(name:, to_value:, decoder:, to_key:, key_decoder:)
}
/// Create a store definition with an out-of-line key.
///
/// Use this when the store was created with `OutOfLineKey` and the primary key
/// is not embedded in the object. Write operations require supplying the key
/// separately via `store.add_with_out_of_line_key` or
/// `store.put_with_out_of_line_key`.
///
pub fn store_with_out_of_line_key(
name name: String,
to_value to_value: fn(t, Action) -> Value,
decoder decoder: decode.Decoder(t),
to_key to_key: fn(k) -> Value,
key_decoder key_decoder: decode.Decoder(k),
) -> Store(store_type, OutOfLineKey, t, k) {
Store(name:, to_value:, decoder:, to_key:, key_decoder:)
}
@internal
pub fn store_name(store: Store(store_type, key_mode, t, k)) -> String {
store.name
}
@internal
pub fn store_to_value(
store: Store(store_type, key_mode, t, k),
) -> fn(t, Action) -> Value {
store.to_value
}
@internal
pub fn store_decoder(
store: Store(store_type, key_mode, t, k),
) -> decode.Decoder(t) {
store.decoder
}
@internal
pub fn store_to_key(
store: Store(store_type, key_mode, t, k),
) -> fn(k) -> Value {
store.to_key
}
@internal
pub fn store_key_decoder(
store: Store(store_type, key_mode, t, k),
) -> decode.Decoder(k) {
store.key_decoder
}
/// Indicates whether a write operation is an insert or an upsert.
///
/// Passed to the `to_value` function of a `Store` so the serializer can
/// produce a different object shape for each case - typically to omit the
/// primary key field on `Add` when IndexedDB generates the key automatically,
/// and to include it on `Put` so the existing record is correctly replaced.
///
pub type Action {
Add
Put
}
/// A definition of an IndexedDB index, bundling its name with the codecs
/// needed to serialize and decode the index key.
///
/// The phantom type `store_type` links the index to its parent store - the
/// compiler will reject any attempt to use an index with a store of a
/// different type.
///
/// Create one with `index` and pass it to `transaction.index` to obtain a
/// handle for use inside a transaction:
///
/// ```gleam
/// pub fn track_artist_index() -> Index(TrackStore, _, _, _) {
/// glindex.index(
/// name: "tracks_artist",
/// to_index_key: fn(artist: String) { glindex.string(artist) },
/// index_key_decoder: decode.string,
/// )
/// }
/// ```
///
pub opaque type Index(store_type, t, k, i) {
Index(
name: String,
to_index_key: fn(i) -> Value,
index_key_decoder: decode.Decoder(i),
)
}
/// Create an index definition.
///
/// `to_index_key` converts a Gleam value into the `Value` used to query the
/// index. `index_key_decoder` reads the raw index key back into a Gleam value
/// when iterating via a cursor.
///
pub fn index(
name name: String,
to_index_key to_index_key: fn(i) -> Value,
index_key_decoder index_key_decoder: decode.Decoder(i),
) -> Index(store_type, t, k, i) {
Index(name:, to_index_key:, index_key_decoder:)
}
@internal
pub fn index_name(index: Index(store_type, t, k, i)) -> String {
index.name
}
@internal
pub fn index_to_index_key(index: Index(store_type, t, k, i)) -> fn(i) -> Value {
index.to_index_key
}
@internal
pub fn index_index_key_decoder(
index: Index(store_type, t, k, i),
) -> decode.Decoder(i) {
index.index_key_decoder
}
/// Hold an IndexedDB database connection.
///
/// Obtained from [`glindex/database.open`](./glindex/database.html#open).
///
pub type Database
@internal
pub type ReadOnly
@internal
pub type ReadWrite
@internal
pub type Normal
@internal
pub type VersionChange
/// An opaque JavaScript value that IndexedDB can store or use as a key.
///
/// Convert Gleam values to `Value` using the helpers in this module:
/// `int`, `string`, `float`, `bool`, `bytea`, `array`, `object`, and so on.
///
pub type Value
/// Specifies which records a store or index operation targets.
///
/// - `All` - every record in the store or index.
/// - `Only(value)` - the single record whose key equals `value`.
/// - `LowerBound(value, exclusive)` - records with key ≥ `value`
/// (or > when `exclusive` is `True`).
/// - `UpperBound(value, exclusive)` - records with key ≤ `value`
/// (or < when `exclusive` is `True`).
/// - `Bound(lower, upper, excl_lower, excl_upper)` - records whose key falls
/// within the given range.
///
pub type Query(v) {
All
Only(v)
LowerBound(value: v, exclusive: Bool)
UpperBound(value: v, exclusive: Bool)
Bound(lower: v, upper: v, exclusive_lower: Bool, exclusive_upper: Bool)
}
/// A `Value` representing JavaScript `null`.
///
@external(javascript, "./glindex_ffi.mjs", "null_value")
pub fn null() -> Value
/// Converts a `Bool` to a `Value`.
///
@external(javascript, "./glindex_ffi.mjs", "coerce")
pub fn bool(a: Bool) -> Value
/// Converts an `Int` to a `Value`.
///
@external(javascript, "./glindex_ffi.mjs", "coerce")
pub fn int(a: Int) -> Value
/// Converts a `Float` to a `Value`.
///
@external(javascript, "./glindex_ffi.mjs", "coerce")
pub fn float(a: Float) -> Value
/// Converts a `String` to a `Value`.
///
@external(javascript, "./glindex_ffi.mjs", "coerce")
pub fn string(a: String) -> Value
/// Converts a `BitArray` to a `Value` (stored as an `ArrayBuffer`).
///
@external(javascript, "./glindex_ffi.mjs", "coerce")
pub fn bytea(a: BitArray) -> Value
/// Converts a list of `Value`s to a JavaScript `Array` `Value`.
///
pub fn array(values: List(Value)) -> Value {
do_array(values)
}
@external(javascript, "./glindex_ffi.mjs", "array")
fn do_array(from: List(Value)) -> Value
/// Converts a list of key-value pairs to a JavaScript object `Value`.
///
/// ## Example
///
/// ```gleam
/// glindex.object([
/// #("title", glindex.string("Bohemian Rhapsody")),
/// #("artist", glindex.string("Queen")),
/// #("duration", glindex.int(354)),
/// ])
/// ```
///
pub fn object(entries: List(#(String, Value))) -> Value {
do_object(entries)
}
@external(javascript, "./glindex_ffi.mjs", "object")
fn do_object(entries: List(#(String, Value))) -> Value
/// Converts a `Dict` to a JavaScript `Map` `Value`.
///
/// Both keys and values are converted with the provided converter functions.
///
pub fn map(
key_converter: fn(k) -> Value,
value_converter: fn(v) -> Value,
dict: Dict(k, v),
) -> Value {
dict.to_list(dict)
|> list.map(fn(entry) {
let #(k, v) = entry
#(key_converter(k), value_converter(v))
})
|> do_map
}
@external(javascript, "./glindex_ffi.mjs", "map_value")
fn do_map(entries: List(#(Value, Value))) -> Value
/// Converts a list to a JavaScript `Set` `Value`.
///
pub fn set(converter: fn(a) -> Value, values: List(a)) -> Value {
list.map(values, converter)
|> do_set
}
@external(javascript, "./glindex_ffi.mjs", "set_value")
fn do_set(values: List(Value)) -> Value
/// Converts a `Timestamp` to a `Value` stored as microseconds since the Unix
/// epoch. Use `timestamp_decoder` to read it back.
///
pub fn timestamp(timestamp: Timestamp) -> Value {
let #(seconds, nanoseconds) =
timestamp.to_unix_seconds_and_nanoseconds(timestamp)
coerce_value(seconds * 1_000_000 + nanoseconds / 1000)
}
/// Decoder for a `Timestamp` stored by `timestamp`.
///
pub fn timestamp_decoder() -> decode.Decoder(Timestamp) {
use microseconds <- decode.map(decode.int)
let seconds = microseconds / 1_000_000
let nanoseconds = { microseconds % 1_000_000 } * 1000
timestamp.from_unix_seconds_and_nanoseconds(seconds, nanoseconds)
}
/// Decoder for a `Date` stored by `calendar_date`.
///
pub fn calendar_date_decoder() -> decode.Decoder(Date) {
use year <- decode.field(0, decode.int)
use month_int <- decode.field(1, decode.int)
use day <- decode.field(2, decode.int)
let month = case month_int {
1 -> calendar.January
2 -> calendar.February
3 -> calendar.March
4 -> calendar.April
5 -> calendar.May
6 -> calendar.June
7 -> calendar.July
8 -> calendar.August
9 -> calendar.September
10 -> calendar.October
11 -> calendar.November
_ -> calendar.December
}
decode.success(calendar.Date(year:, month:, day:))
}
/// Decoder for a `TimeOfDay` stored by `calendar_time_of_day`.
///
pub fn calendar_time_of_day_decoder() -> decode.Decoder(TimeOfDay) {
use hours <- decode.field(0, decode.int)
use minutes <- decode.field(1, decode.int)
use float_seconds <- decode.field(2, decode.float)
let seconds = float.truncate(float_seconds)
let nanoseconds =
float.round({ float_seconds -. int.to_float(seconds) } *. 1_000_000_000.0)
decode.success(calendar.TimeOfDay(hours:, minutes:, seconds:, nanoseconds:))
}
/// Converts a `Date` to an array `Value` of `[year, month, day]`.
/// Use `calendar_date_decoder` to read it back.
///
pub fn calendar_date(date: Date) -> Value {
let month = calendar.month_to_int(date.month)
coerce_value(#(date.year, month, date.day))
}
/// Converts a `TimeOfDay` to an array `Value` of `[hours, minutes, seconds]`.
/// Use `calendar_time_of_day_decoder` to read it back.
///
pub fn calendar_time_of_day(time: TimeOfDay) -> Value {
let seconds = int.to_float(time.seconds)
let seconds = seconds +. int.to_float(time.nanoseconds) /. 1_000_000_000.0
coerce_value(#(time.hours, time.minutes, seconds))
}
@external(javascript, "./glindex_ffi.mjs", "coerce")
fn coerce_value(a: anything) -> Value
/// Compares two `Value`s using IndexedDB's built-in key comparison algorithm.
///
/// This mirrors the ordering IndexedDB uses internally for keys and ranges,
/// which differs from JavaScript's default `<` / `>` operators for certain
/// types such as arrays.
///
pub fn cmp(a: Value, b: Value) -> Order {
case cmp_ffi(a, b) {
1 -> order.Gt
-1 -> order.Lt
_ -> order.Eq
}
}
@external(javascript, "./glindex_ffi.mjs", "cmp")
fn cmp_ffi(a: Value, b: Value) -> Int