npm package: @neabyte/indexed-orm
Description: Async IndexedDB ORM, lightweight, and ready for modern runtimes
Author: NeaByteLab
Latest version: 0.1.0

# Indexed-ORM [![Module type: Deno/ESM](https://img.shields.io/badge/module%20type-deno%2Fesm-brightgreen)](https://github.com/NeaByteLab/Indexed-ORM) [![npm version](https://img.shields.io/npm/v/@neabyte/indexed-orm.svg)](https://www.npmjs.org/package/@neabyte/indexed-orm) [![JSR](https://jsr.io/badges/@neabyte/indexed-orm)](https://jsr.io/@neabyte/indexed-orm) [![CI](https://github.com/NeaByteLab/Indexed-ORM/actions/workflows/ci.yaml/badge.svg)](https://github.com/NeaByteLab/Indexed-ORM/actions/workflows/ci.yaml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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

## Installation

### Deno

```bash
deno add jsr:@neabyte/indexed-orm
```

### npm

```bash
npm install @neabyte/indexed-orm
```

### CDN (browser / any ESM)

```html
<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?)`.
- **Types** – `FindOptions`, `StoreHandle`, `OpenOptions`, `Builder`, `Chain`, `KeyRangeOptions`, `IDBValidKey`, `TransactionDurability`, and others.

```typescript
import { Database, Range, Store } from '@neabyte/indexed-orm'
import type { FindOptions, StoreHandle, OpenOptions } from '@neabyte/indexed-orm'
```

## Usage

### Quick start

```typescript
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.

```typescript
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`):

```typescript
const users = db.store('users')
```

### StoreHandle (CRUD)

```typescript
// 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:

```typescript
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:

```typescript
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):

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

Additional filters:

- **`where`** — **Client-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.

```typescript
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)

```typescript
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:

```typescript
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.

```typescript
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`).

```typescript
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:

```typescript
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

```typescript
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

```typescript
db.store(storeName)
```

- `storeName` `<string>`: Object store name.
- Returns: `StoreHandle<unknown>`
- Description: Returns store handle bound to store name.

### Store.createStore

```typescript
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

```typescript
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

```typescript
store.put(item)
```

- `item` `<T>`: Item with keyPath field.
- Returns: `Promise<IDBValidKey>`
- Description: Puts item; overwrites existing item with same key.

### StoreHandle.putMany

```typescript
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

```typescript
store.get(key)
```

- `key` `<IDBValidKey>`: Primary key.
- Returns: `Promise<T | undefined>`
- Description: Returns one item by key, or undefined when missing.

### StoreHandle.update

```typescript
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

```typescript
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

```typescript
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

```typescript
store.clear()
```

- Returns: `Promise<void>`
- Description: Clears all items in the store in one transaction.

### StoreHandle.find

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
store.keys(options?)
```

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

### StoreHandle.primaryKeys

```typescript
store.primaryKeys(options?)
```

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

### StoreHandle.uniqueKeys

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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<T>** (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

- [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) — MDN Web APIs

## License

This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for details.