Skip to content

Async IndexedDB ORM, lightweight, and ready for modern runtimes.

License

Notifications You must be signed in to change notification settings

NeaByteLab/Indexed-ORM

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Indexed-ORM Module type: Deno/ESM npm version JSR CI License

Async IndexedDB ORM, lightweight, and ready for modern runtimes.

Installation

Deno

deno add jsr:@neabyte/indexed-orm

npm

npm install @neabyte/indexed-orm

CDN (browser / any ESM)

<script type="module">
  import { Database } from 'https://esm.sh/jsr/@neabyte/indexed-orm'
  // or pin version: .../indexed-orm@1.0.0
</script>
  • Latest: https://esm.sh/jsr/@neabyte/indexed-orm
  • Pinned: https://esm.sh/jsr/@neabyte/indexed-orm@<version>

Package exports

You can import the following from @neabyte/indexed-orm:

  • Database – open DB and run schema upgrade.
  • Range – build IDBKeyRange from options (e.g. for custom cursor logic).
  • Store – advanced: create a store handle from an existing IDBDatabase via Store.createStore(db, storeName, keyPath, options?).
  • TypesFindOptions, StoreHandle, OpenOptions, Builder, Chain, KeyRangeOptions, IDBValidKey, TransactionDurability, and others.
import { Database, Range, Store } from '@neabyte/indexed-orm'
import type { FindOptions, StoreHandle, OpenOptions } from '@neabyte/indexed-orm'

Usage

Quick start

import { Database } from '@neabyte/indexed-orm'

const db = await Database.open('MyApp', 1, builder => {
  builder.store('users', 'id').index('email')
})
const users = db.store('users')
await users.put({ id: '1', email: 'a@b.com', name: 'Alice' })
const user = await users.get('1')

Database

Open a database by name and version. The upgrade callback runs on first open and on version bumps. The second argument to store() is the key path (e.g. 'id') — the property on your objects used as the primary key.

Version and data: Bumping the version (e.g. 1 → 2) only reruns the upgrade callback so you can create new stores or indexes. Existing data is kept unless you explicitly delete object stores or clear them inside upgrade. Data is lost only if you drop a store or the user clears site data.

import { Database } from '@neabyte/indexed-orm'

const db = await Database.open('MyApp', 1, builder => {
  builder.store('users', 'id').index('email').index('name')
  builder.store('posts', 'id').index('userId').index('createdAt')
})

// Optional: openOptions (e.g. relaxed durability for better write perf where allowed)
const dbRelaxed = await Database.open(
  'MyApp',
  1,
  builder => {
    builder.store('users', 'id')
  },
  { durability: 'relaxed' }
)

Get a store handle (db is a DBHandle):

const users = db.store('users')

StoreHandle (CRUD)

// add(item, key?): with keyPath store, the key is taken from item[keyPath]; pass key only to override or when the item has no keyPath property
await users.add({ id: '1', email: 'a@b.com', name: 'Alice' })
await users.put({ id: '1', email: 'a@b.com', name: 'Alice Updated' })

// Read, update, delete
const user = await users.get('1')
await users.update('1', { name: 'Alice Smith' })
await users.delete('1')

// Bulk and clear
await users.putMany([
  { id: '1', email: 'a@b.com' },
  { id: '2', email: 'b@b.com' }
])
await users.deleteMany(['1', '2'])
await users.clear()

StoreHandle (FindOptions)

Basic find with index, prefix, ordering, limit, and offset:

const out = await users.find({
  index: 'email',
  prefix: 'a',
  order: 'asc',
  limit: 10,
  offset: 0
})

Range queries (eq, min, max, includeMin, includeMax). Use eq with no index to match by primary key; use index to match by that index:

await users.find({ eq: '1' })
await users.find({ index: 'email', eq: 'alice@b.com' })
await users.find({ index: 'createdAt', min: 1000, max: 2000, includeMin: true, includeMax: false })

Multiple prefix match (prefixes — match if key starts with any of the given strings):

await users.find({ index: 'email', prefixes: ['alice', 'admin'] })

Additional filters:

  • whereClient-side only. The predicate runs in JS while iterating or filtering results; it is not pushed to IndexedDB indexes. Use it for flexible conditions (e.g. item.role === 'admin'), but on large datasets index-based options (eq, prefix, min/max) are faster because they use the engine’s range scan.
  • ignoreCase — Applies only to eq, prefix, and prefixes (case-insensitive string comparison). It does not affect min/max or other range options. Comparison is done in JS after the range is applied.
const out = await users.find({
  anyOf: ['id1', 'id2'],
  noneOf: ['id3'],
  notEq: 'id4',
  where: item => item.role === 'admin',
  distinct: true,
  ignoreCase: true
})

For custom key ranges (e.g. multiple intervals), use the ranges option with pre-built IDBKeyRange objects — build them with Range.buildKeyRange(opts).

StoreHandle (Count, First, Last, Keys)

await users.count({ prefix: 'a' })
await users.first({ order: 'asc' })
await users.last({ order: 'desc' })
await users.keys({ limit: 100 })
await users.primaryKeys({ limit: 100 })
await users.uniqueKeys('email', {})

StoreHandle (each, eachKey, eachUniqueKey)

Return false from the callback to stop iteration:

await users.each(item => {
  if (item.id === 'stop') {
    return false
  }
})

await users.eachKey(key => {
  console.log(key)
})

await users.eachUniqueKey('email', key => {
  console.log(key)
})

StoreHandle (modify)

modify updates records by cursor. Return a partial object to merge into each record, or false to stop iteration. Each modify call runs inside a single readwrite transaction (atomic). Two overlapping modify calls are separate transactions; IndexedDB does not lock across them, so if two modify calls touch the same records at the same time, the last write wins. For predictable results, avoid concurrent modify on the same store or sequence writes in your app.

await users.modify(item => ({ ...item, role: 'user' }), { where: item => item.role === 'guest' })

Transaction scope

Each store method uses its own transaction (e.g. one putMany = one readwrite transaction; one find = one readonly transaction). The ORM does not expose cross-store or custom multi-operation transactions. If you need a single atomic transaction across multiple stores (e.g. deduct from accounts and append to ledger), use raw IndexedDB: open the database, start a transaction with multiple store names, and perform the operations on transaction.objectStore(...).

Advanced: Store.createStore

If you already have an IDBDatabase (e.g. from another library or custom indexedDB.open()), you can create a store handle without Database.open. The database must already have the object store (e.g. created in onupgradeneeded).

import { Store } from '@neabyte/indexed-orm'

const rawDb = await new Promise<IDBDatabase>((resolve, reject) => {
  const r = indexedDB.open('MyApp', 1)
  r.onupgradeneeded = () => {
    r.result.createObjectStore('users', { keyPath: 'id' })
  }
  r.onsuccess = () => resolve(r.result)
  r.onerror = () => reject(r.error)
})
const users = Store.createStore(rawDb, 'users', 'id')
// same StoreHandle API: users.get(), users.find(), etc.

Error handling

All store methods return Promises. Rejections can come from IndexedDB (e.g. quota exceeded, database blocked by user, or transaction aborted). For robust apps, wrap calls in try/catch and handle known cases:

try {
  await users.put(largeItem)
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    // Storage full; prompt user or evict old data
  }
  if (e.name === 'SecurityError' || e.name === 'UnknownError') {
    // e.g. private browsing or user disabled storage
  }
  throw e
}

Errors from Database.open (e.g. version conflict or blocked) also need handling if you support multiple tabs or strict reliability.

Modules Feature List

Method Category Description
Database.open Database Opens DB and runs upgrade callback.
DBHandle.store Database Returns store handle by name.
Store.createStore Store Returns store handle from existing IDBDatabase.
StoreHandle.add StoreHandle Adds item; optional key when applicable.
StoreHandle.put StoreHandle Puts item (overwrites by key).
StoreHandle.get StoreHandle Gets one item by key.
StoreHandle.update StoreHandle Merges changes into item at key.
StoreHandle.delete StoreHandle Deletes item by key.
StoreHandle.putMany StoreHandle Puts many items in one transaction.
StoreHandle.deleteMany StoreHandle Deletes many keys in one transaction.
StoreHandle.clear StoreHandle Clears all items in store.
StoreHandle.find StoreHandle Finds items matching FindOptions.
StoreHandle.count StoreHandle Counts items matching FindOptions.
StoreHandle.first StoreHandle Returns first item matching options.
StoreHandle.last StoreHandle Returns last item matching options.
StoreHandle.keys StoreHandle Returns primary keys matching options.
StoreHandle.primaryKeys StoreHandle Alias for keys.
StoreHandle.uniqueKeys StoreHandle Returns deduped keys from index.
StoreHandle.each StoreHandle Iterates matching items; stop with false.
StoreHandle.eachKey StoreHandle Iterates matching keys; stop with false.
StoreHandle.eachUniqueKey StoreHandle Iterates index keys; stop with false.
StoreHandle.modify StoreHandle Cursor update; returns updated count.
Range.buildKeyRange Range Builds IDBKeyRange from range options.

API Reference

Database.open

Database.open(name, version, upgrade?, openOptions?)
  • name <string>: Database name.
  • version <number>: Schema version; bump to rerun upgrade.
  • upgrade <(db: Builder) => void>: (Optional) Defines stores and indexes.
  • openOptions <OpenOptions>: (Optional) { durability?: 'default' | 'relaxed' }. Defaults to {}.
  • Returns: Promise<DBHandle>
  • Description: Opens DB, runs upgrade, and returns handle.

DBHandle.store

db.store(storeName)
  • storeName <string>: Object store name.
  • Returns: StoreHandle<unknown>
  • Description: Returns store handle bound to store name.

Store.createStore

Store.createStore(db, storeName, keyPath, options?)
  • db <IDBDatabase>: Opened IndexedDB database.
  • storeName <string>: Object store name.
  • keyPath <string>: Primary key path (e.g. 'id').
  • options <CreateStoreOptions>: (Optional) { durability?: 'default' | 'relaxed' }.
  • Returns: StoreHandle<T>
  • Description: Creates a store handle when you already have an IDBDatabase (e.g. from custom indexedDB.open() or another library).

StoreHandle.add

store.add(item, key?)
  • item <T>: Item to add.
  • key <IDBValidKey>: (Optional) The key is taken from item[keyPath], so usually omitted. Pass key only to override that key or when the item does not have the keyPath property.
  • Returns: Promise<IDBValidKey>
  • Description: Adds item; fails if a record with the same key already exists (use put to overwrite).

StoreHandle.put

store.put(item)
  • item <T>: Item with keyPath field.
  • Returns: Promise<IDBValidKey>
  • Description: Puts item; overwrites existing item with same key.

StoreHandle.putMany

store.putMany(items)
  • items <T[]>: Array of items to put.
  • Returns: Promise<void>
  • Description: Puts multiple items in one transaction; no-op when not an array.

StoreHandle.get

store.get(key)
  • key <IDBValidKey>: Primary key.
  • Returns: Promise<T | undefined>
  • Description: Returns one item by key, or undefined when missing.

StoreHandle.update

store.update(key, changes)
  • key <IDBValidKey>: Primary key.
  • changes <Partial<T>>: Fields to merge into existing item.
  • Returns: Promise<void>
  • Description: Gets existing item, merges changes, puts back; no-op when key missing.

StoreHandle.delete

store.delete(key)
  • key <IDBValidKey>: Primary key to delete.
  • Returns: Promise<void>
  • Description: Deletes item by key; no-op when key null or undefined.

StoreHandle.deleteMany

store.deleteMany(keys)
  • keys <IDBValidKey[]>: Array of keys to delete.
  • Returns: Promise<void>
  • Description: Deletes multiple keys in one transaction; no-op when not an array.

StoreHandle.clear

store.clear()
  • Returns: Promise<void>
  • Description: Clears all items in the store in one transaction.

StoreHandle.find

store.find(options?)
  • options <FindOptions<T>>: (Optional) Filter and query options. Defaults to {}.
  • Returns: Promise<T[]>
  • Description: Returns items matching options and filters. Options like eq, prefix, min/max use the index/range; where, anyOf, noneOf, distinct, ignoreCase are applied client-side (see Option Types).

StoreHandle.count

store.count(options?)
  • options <FindOptions<T>>: (Optional) Same filters as find.
  • Returns: Promise<number>
  • Description: Counts items matching options; uses range count when possible.

StoreHandle.first

store.first(options?)
  • options <FindOptions<T>>: (Optional) Same filters as find; order applies.
  • Returns: Promise<T | undefined>
  • Description: Returns first item matching options, or undefined.

StoreHandle.last

store.last(options?)
  • options <FindOptions<T>>: (Optional) Same filters as find; order applies.
  • Returns: Promise<T | undefined>
  • Description: Returns last item matching options, or undefined.

StoreHandle.keys

store.keys(options?)
  • options <FindOptions<T>>: (Optional) Same filters as find.
  • Returns: Promise<IDBValidKey[]>
  • Description: Returns primary keys matching options.

StoreHandle.primaryKeys

store.primaryKeys(options?)
  • options <FindOptions<T>>: (Optional) Same as keys.
  • Returns: Promise<IDBValidKey[]>
  • Description: Alias for keys; returns primary keys matching options.

StoreHandle.uniqueKeys

store.uniqueKeys(indexName?, options?)
  • indexName <string>: (Optional) Index name; when omitted, behaves like keys.
  • options <FindOptions<T>>: (Optional) Range and filter options.
  • Returns: Promise<IDBValidKey[]>
  • Description: Returns deduped keys from index; falls back to keys when indexName omitted.

StoreHandle.each

store.each(callback, options?)
  • callback <(item: T) => void | false>: Invoked per item; return false to stop.
  • options <FindOptions<T>>: (Optional) Same filters as find.
  • Returns: Promise<void>
  • Description: Iterates matching items; return false from callback to stop.

StoreHandle.eachKey

store.eachKey(callback, options?)
  • callback <(key: IDBValidKey) => void | false>: Invoked per key; return false to stop.
  • options <FindOptions<T>>: (Optional) Same filters as find.
  • Returns: Promise<void>
  • Description: Iterates matching keys only; return false to stop.

StoreHandle.eachUniqueKey

store.eachUniqueKey(indexName, callback, options?)
  • indexName <string>: Index name (required).
  • callback <(key: IDBValidKey) => void | false>: Invoked per unique key; return false to stop.
  • options <FindOptions<T>>: (Optional) Range options.
  • Returns: Promise<void>
  • Description: Iterates unique keys of index; key cursor; indexName required.

StoreHandle.modify

store.modify(changeCallback, options?)
  • changeCallback <(item: T) => void | Partial<T> | false>: Callback per item; object merges; false stops.
  • options <FindOptions<T>>: (Optional) Select items to modify. Defaults to {}.
  • Returns: Promise<number>
  • Description: Updates records by cursor; returns updated count.

Range.buildKeyRange

Range.buildKeyRange(opts)
  • opts <KeyRangeOptions>: Key range options.
  • Returns: IDBKeyRange | null
  • Description: Builds IDBKeyRange for cursor queries.

Option Types

  • IDBValidKey: key type for get, delete, add( key ), etc. — string | number | Date | ArrayBufferView | ArrayBuffer.
  • OpenOptions (optional): durability ('default' | 'relaxed').
  • CreateStoreOptions (optional): durability ('default' | 'relaxed').
  • Builder (passed to Database.open upgrade): store(storeName, keyPath) → returns Chain; use to define stores and indexes.
  • Chain (from builder.store()): index(indexName) → returns Chain; add index names for the store.
  • KeyRangeOptions (optional): eq, min, max, includeMin, includeMax, prefix.
  • FindOptions (optional): index, order, eq, notEq, min, max, includeMin, includeMax, prefix, prefixes, ignoreCase (for eq/prefix/prefixes only), anyOf, noneOf, where (client-side filter), distinct, ranges, limit, offset.
  • StoreSchemaConfig (internal): { keyPath, indexes } — schema shape per store (from upgrade).

Reference

License

This project is licensed under the MIT license. See the LICENSE file for details.