ark-indexdb
v0.0.2
Published
simple library to manage index db
Maintainers
Readme
My own convenient js for IndexDB!
Simple javascript functions to manage IndexDB
╔═══════════════════════════════════════════════════════════╗
║ ArkIndexDb v1.0.0 ║
║ A complete, production-grade IndexedDB wrapper library ║
║ 100% browser-native · zero dependencies · ES2020+ ║
╚═══════════════════════════════════════════════════════════╝Table of Contents
- What is ArkIndexDb?
- Features
- Browser Support
- Installation
- Quick Start
- Schema & Indexes
- API Reference
- Filter Operators
- Patch Operators
- Full Method List
- Files
- License
What is ArkIndexDb?
ArkIndexDb is a thin but powerful wrapper around the browser's native IndexedDB API. IndexedDB is the browser's built-in persistent key-value / object store — it can hold hundreds of megabytes of structured data, survives page reloads and browser restarts, and works completely offline with no server required.
The raw IndexedDB API is notoriously verbose and callback-heavy. ArkIndexDb fixes that by providing:
- A clean Promise-based async/await API
- A fluent Query Builder for chained, readable queries
- Schema-driven store and index creation
- Field-level patch operators (
$inc,$push,$toggle, …) - Multi-store atomic transactions
- A built-in event emitter for reactive data flows
- JSON import / export and one-call browser file download
Everything runs 100% locally in the browser. No network requests, no backend, no npm, no bundler required.
Features
| Feature | Description |
|---|---|
| 🏗 Schema-driven | Declare stores and indexes upfront; ArkIndexDb creates them automatically |
| ⚡ Full CRUD | insert, insertMany, findById, findAll, update, delete, and more |
| 🔍 Rich queries | Filter by field values, operators, or predicate functions |
| 📑 Native indexes | findByIndex and findByRange use IDB's O(log n) index lookups |
| 📄 Pagination | Built-in paginate() with page/pageSize, totalPages, hasNext, hasPrev |
| 🔗 Query Builder | Fluent chainable API: .where().sortBy().limit().page().exec() |
| 🧩 Patch operators | $set, $inc, $dec, $mul, $push, $pull, $toggle, $unset |
| 🔁 Upsert | Insert-or-replace via native IDB put() |
| ⚛️ Transactions | Multi-store atomic operations with automatic rollback on error |
| 🖱 Cursor iteration | Memory-efficient record traversal for very large stores |
| 📤 Export | JSON export of one store or all stores; triggers browser file download |
| 📥 Import | Import records from a JSON string or file |
| 📡 Event emitter | React to insert, update, delete, clear, error, and more |
| 🕑 Auto-timestamps | _createdAt and _updatedAt ISO strings added automatically |
| 🆔 Auto UUID | Primary keys auto-generated via crypto.getRandomValues() |
Browser Support
ArkIndexDb uses ES2020 private class fields (#) and async/await.
| Browser | Minimum Version | |---|---| | Chrome / Edge | 74+ | | Firefox | 90+ | | Safari | 14.1+ | | Opera | 62+ |
Note: IndexedDB itself is available in all modern browsers and many older ones. The ES2020 syntax is the limiting factor for ArkIndexDb. If you need to support older browsers, transpile with Babel.
Demo Link
You may be using Demo Link.
Installation
Option 1 — npm install
npm i ark-indexdbOption 1 — Script tag (recommended, zero build step)
Download ark-indexdb.js and place it alongside your HTML:
<script src="https://cdn.jsdelivr.net/npm/ark-indexdb@latest/ark-indexdb.js"></script>
<!-- ArkIndexDb and ArkQueryBuilder are now on window -->Option 2 — Inline
Copy the contents of ark-indexdb.js directly into a <script> block in your HTML file. Useful for single-file apps.
Option 3 — ES Module (with a bundler)
Add export statements to ark-indexdb.js and import:
import { ArkIndexDb } from './ark-indexdb.js';Quick Start
// 1. Create an instance
const ark = new ArkIndexDb();
// 2. Open the database — define your schema here
await ark.open('MyAppDB', 1, {
users: {
keyPath: 'id',
autoIncrement: false,
indexes: [
{ name: 'by_email', keyPath: 'email', unique: true },
{ name: 'by_role', keyPath: 'role', unique: false },
]
}
});
// 3. INSERT — UUID id is auto-generated
const key = await ark.insert('users', {
name: 'Alice Chen',
email: '[email protected]',
role: 'admin',
});
// 4. READ by primary key
const user = await ark.findById('users', key);
console.log(user.name); // "Alice Chen"
// 5. FIND with a filter
const { data, total } = await ark.findAll('users', {
filter: { role: 'admin' },
sort: 'name',
order: 'asc',
});
// 6. UPDATE (partial merge — only listed fields change)
await ark.update('users', key, { city: 'Berlin' });
// 7. DELETE
await ark.delete('users', key);Schema & Indexes
Pass your schema as the third argument to open(). Each top-level key becomes an object store. Declaring indexes lets you use findByIndex() and findByRange() for native, fast O(log n) lookups.
const schema = {
posts: {
keyPath: 'id', // primary key field name (default: 'id')
autoIncrement: false, // set true for auto-incrementing numeric keys
indexes: [
{ name: 'by_author', keyPath: 'authorId', unique: false },
{ name: 'by_category', keyPath: 'category', unique: false },
{ name: 'by_slug', keyPath: 'slug', unique: true },
]
},
comments: {
keyPath: 'id',
indexes: [
{ name: 'by_post', keyPath: 'postId', unique: false },
]
},
settings: {
keyPath: 'key', // any field can be the primary key
}
};
await ark.open('BlogDB', 1, schema);Schema Upgrades
To add new stores or indexes to an existing database, increment the version number. ArkIndexDb's onupgradeneeded handler runs automatically:
// Version 1 → 2 adds a new store and a new index
await ark.open('BlogDB', 2, {
...schema,
tags: { keyPath: 'id' }
});API Reference
Lifecycle
ark.open(name, version, schema) → Promise<ArkIndexDb>
Opens or creates an IndexedDB database. Always await this before any other call.
const ark = new ArkIndexDb();
await ark.open('AppDB', 1, schema);ark.close() → void
Closes the active database connection.
ArkIndexDb.dropDatabase(name) → Promise<boolean> (static)
Permanently deletes a database by name.
await ArkIndexDb.dropDatabase('AppDB');ArkIndexDb.listDatabases() → Promise<Array<{name, version}>> (static)
Lists all IndexedDB databases in the current origin.
const dbs = await ArkIndexDb.listDatabases();Getters
| Property | Type | Description |
|---|---|---|
| ark.isOpen | boolean | Whether the database is currently open |
| ark.dbName | string | Database name |
| ark.dbVersion | number | Database version |
| ark.storeNames | string[] | All object store names |
Create
ark.insert(storeName, data) → Promise<string>
Inserts a single record. A UUID id is auto-generated if the keyPath is 'id' and no id is provided. Adds _createdAt and _updatedAt ISO timestamps automatically.
const key = await ark.insert('users', {
name: 'Bob',
email: '[email protected]',
role: 'user',
tags: ['beta', 'early-adopter'],
});
// key → UUID string e.g. "df_lxyz1_a8b3c"ark.insertMany(storeName, records[]) → Promise<string[]>
Inserts multiple records in a single IDB transaction. Significantly faster than calling insert() in a loop for bulk data.
const keys = await ark.insertMany('products', [
{ name: 'Widget A', price: 9.99 },
{ name: 'Widget B', price: 14.99 },
{ name: 'Gadget X', price: 49.99 },
]);
// keys → ['uuid-1', 'uuid-2', 'uuid-3']Read
ark.findById(storeName, id) → Promise<Object | null>
Fetches a single record by its primary key. Returns null if not found.
const user = await ark.findById('users', 'abc-123');ark.findAll(storeName, options?) → Promise<{ data, total }>
Fetches all records with optional in-memory filter, sort, limit, and offset.
const { data, total } = await ark.findAll('users', {
filter: { role: 'admin', age: { $gte: 18 } },
sort: 'name',
order: 'asc', // 'asc' | 'desc'
limit: 10,
offset: 0,
});data— the matching records array (after pagination)total— total matching count before limit/offset
ark.findByIndex(storeName, indexName, value, options?) → Promise<{ data, total }>
Uses a native IDB index for fast lookups. The index must be declared in the schema.
// Find all admins using the native 'by_role' index
const { data } = await ark.findByIndex('users', 'by_role', 'admin');
// Combine with additional in-memory filtering
const { data } = await ark.findByIndex(
'users', 'by_role', 'admin',
{ filter: { status: 'active' }, sort: 'name' }
);ark.findByRange(storeName, indexName, range, options?) → Promise<{ data, total }>
Performs a key range query on a named index.
// Products priced between $10 and $50 (inclusive)
const { data } = await ark.findByRange('products', 'by_price', {
lower: 10,
upper: 50,
});
// Price strictly greater than $10 (exclusive lower bound)
const { data } = await ark.findByRange('products', 'by_price', {
lower: 10,
lowerOpen: true, // open = exclusive
});
// All prices up to $50
const { data } = await ark.findByRange('products', 'by_price', {
upper: 50,
});Range options:
| Option | Type | Default | Description |
|---|---|---|---|
| lower | any | — | Lower bound value |
| upper | any | — | Upper bound value |
| lowerOpen | boolean | false | Exclude the lower bound itself |
| upperOpen | boolean | false | Exclude the upper bound itself |
ark.findOne(storeName, filter) → Promise<Object | null>
Returns the first matching record, or null if none found.
const admin = await ark.findOne('users', { role: 'admin' });
const alice = await ark.findOne('users', r => r.email === '[email protected]');ark.exists(storeName, id) → Promise<boolean>
if (await ark.exists('users', id)) {
console.log('User found');
}ark.count(storeName, filter?) → Promise<number>
Uses native IDB count() when no filter is supplied (fastest path).
const total = await ark.count('users');
const admins = await ark.count('users', { role: 'admin' });
const active = await ark.count('users', r => r.status === 'active');ark.getAllKeys(storeName) → Promise<Array>
Returns all primary keys without fetching full record data.
const ids = await ark.getAllKeys('users');ark.paginate(storeName, page, pageSize, options?) → Promise<PaginationResult>
const result = await ark.paginate('users', 1, 10, {
filter: { status: 'active' },
sort: 'name',
order: 'asc',
});
result.data // current page records
result.total // total matching records
result.page // current page number
result.pageSize // records per page
result.totalPages // total page count
result.hasNext // true if a next page exists
result.hasPrev // true if a previous page existsUpdate
ark.update(storeName, id, changes) → Promise<Object>
Partial merge — only the fields you specify are changed. All unmentioned fields are preserved. Throws if the record doesn't exist. Updates _updatedAt automatically.
// Only 'name' and 'city' change; email, role, etc. are untouched
const updated = await ark.update('users', id, {
name: 'Alice Smith',
city: 'Berlin',
});ark.upsert(storeName, data) → Promise<string>
Insert or replace via native IDB put(). Does not require the record to exist first. If the key exists, the full record is replaced.
// Creates the setting if it doesn't exist, overwrites if it does
await ark.upsert('settings', { id: 'theme', value: 'dark' });
await ark.upsert('settings', { id: 'theme', value: 'light' }); // overwritesark.patch(storeName, id, ops) → Promise<Object>
Field-level patch operators — atomically mutate specific fields without manually reading and rewriting the full record.
await ark.patch('users', id, {
$set: { city: 'Tokyo', bio: 'Developer' }, // set fields
$inc: { loginCount: 1 }, // add 1
$dec: { credits: 5 }, // subtract 5
$mul: { score: 1.5 }, // multiply by 1.5
$toggle: { isVerified: true }, // flip boolean
$push: { tags: 'vip' }, // append to array
$pull: { tags: 'beta' }, // remove from array
$unset: ['temporaryField'], // delete field
});See Patch Operators for the full table.
ark.updateWhere(storeName, filter, changes) → Promise<Object[]>
Updates all records matching a filter. Returns an array of all updated records.
// Deactivate all guest users
const updated = await ark.updateWhere(
'users',
{ role: 'guest' },
{ status: 'inactive' }
);
console.log(`Updated ${updated.length} users`);Delete
ark.delete(storeName, id) → Promise<boolean>
Deletes a single record by primary key. Throws if the record doesn't exist.
await ark.delete('users', id);ark.deleteWhere(storeName, filter) → Promise<number>
Deletes all records matching a filter. Returns the count of deleted records.
// Prune expired sessions
const n = await ark.deleteWhere('sessions', {
expiresAt: { $lt: new Date().toISOString() }
});
console.log(`Pruned ${n} expired sessions`);ark.clear(storeName) → Promise<number>
Deletes all records in a store while keeping the store structure intact. Returns the count of deleted records.
await ark.clear('logs');Query Builder
A fluent, chainable API. Call ark.query(storeName) to get a builder, chain methods, then call one of the terminal methods (.exec(), .count(), .first()).
// Fetch records
const { data } = await ark
.query('users')
.where({ role: 'admin', age: { $gte: 25 } })
.sortBy('name', 'asc')
.limit(10)
.offset(0)
.exec();
// Count without fetching data
const n = await ark.query('users')
.where({ status: 'active' })
.count();
// First matching record
const alice = await ark.query('users')
.where({ email: '[email protected]' })
.first();
// Paginate via builder (.page(pageNumber, pageSize))
const { data } = await ark.query('posts')
.where({ category: 'tech' })
.sortBy('_createdAt', 'desc')
.page(2, 20)
.exec();Builder Methods
| Method | Description |
|---|---|
| .where(filter) | Set a filter object or predicate function |
| .sortBy(field, order?) | Sort by field, 'asc' (default) or 'desc' |
| .limit(n) | Maximum number of records to return |
| .offset(n) | Number of records to skip |
| .page(p, size?) | Shorthand for limit + offset for page p |
| .exec() | Execute → returns { data, total } |
| .count() | Execute count only → returns number |
| .first() | Execute, return first result or null |
Transactions
Run multiple operations across one or more stores atomically. If any step throws, the entire transaction is automatically rolled back.
await ark.transaction(
['users', 'orders'], // all stores involved
'readwrite',
async (stores) => {
// Both inserts succeed together — or both roll back
const userId = crypto.randomUUID();
await stores.users.add({
id: userId,
name: 'Dave',
_createdAt: new Date().toISOString(),
_updatedAt: new Date().toISOString(),
});
await stores.orders.add({
id: crypto.randomUUID(),
userId,
total: 149.99,
status: 'pending',
});
}
);The stores proxy exposes promisified versions of the native IDB methods:
add · put · get · delete · clear · getAll · count · getAllKeys · index(name)
Cursor Iteration
For very large stores, loading all records into memory with getAll() may be expensive. Use iterateCursor() to process records one at a time:
await ark.iterateCursor('logs', record => {
if (record.level === 'error') {
console.error(record.message);
}
// return false to stop iteration early
});
// Reverse direction
await ark.iterateCursor('events', record => {
process(record);
}, 'prev');Direction options: 'next' (default) · 'prev' · 'nextunique' · 'prevunique'
Import / Export
ark.exportStore(storeName) → Promise<string>
Exports all records from a store as a formatted JSON string.
const json = await ark.exportStore('users');
localStorage.setItem('users_backup', json);ark.exportAll() → Promise<string>
Exports every store in the database into a single JSON string.
const fullBackup = await ark.exportAll();ark.downloadStore(storeName) → Promise<void>
Triggers a browser file download of the store's JSON export.
await ark.downloadStore('users');
// Browser saves: "users_1700000000000.json"ark.importStore(storeName, json) → Promise<number>
Imports records from a JSON string previously created by exportStore(). Returns the count of imported records.
// From a fetch response
const json = await fetch('/backup/users.json').then(r => r.text());
const n = await ark.importStore('users', json);
console.log(`Imported ${n} records`);
// From a file input
fileInput.addEventListener('change', async e => {
const text = await e.target.files[0].text();
await ark.importStore('users', text);
});Events
Listen to lifecycle operations with ark.on(event, callback). The returned value is an unsubscribe function.
// Subscribe to a specific event
const off = ark.on('insert', ({ storeName, key, data }) => {
console.log(`Inserted into ${storeName}: key=${key}`);
});
// Wildcard — receives every event
ark.on('*', e => console.log('[ArkIndexDb]', e.event, e));
// Unsubscribe
off();All Available Events
| Event | Payload | Description |
|---|---|---|
| open | { name, version } | DB opened successfully |
| upgrade | { oldVersion, newVersion } | DB schema upgraded |
| insert | { storeName, key, data } | One record inserted |
| insertMany | { storeName, count } | Batch insert completed |
| update | { storeName, id, before, after } | Record updated |
| updateMany | { storeName, count } | Bulk update completed |
| upsert | { storeName, key } | Record upserted |
| delete | { storeName, id, record } | Record deleted |
| deleteMany | { storeName, count } | Bulk delete completed |
| clear | { storeName, count } | Store cleared |
| import | { storeName, count } | Import completed |
| transaction | { storeNames, mode, status } | Transaction committed or errored |
| error | IDB error object | Any IndexedDB error |
| versionchange | {} | Another tab opened a newer DB version |
| blocked | {} | DB open blocked by another connection |
| * | { event, ...payload } | Wildcard — all of the above |
Filter Operators
Use operators inside filter objects for findAll(), findOne(), updateWhere(), deleteWhere(), and the Query Builder's .where().
// Shorthand (exact equality)
{ role: 'admin' }
// With operator
{ age: { $gte: 18 }, status: { $ne: 'banned' } }| Operator | Description | Example |
|---|---|---|
| $eq | Equal to (same as plain value) | { age: { $eq: 30 } } |
| $ne | Not equal to | { status: { $ne: 'banned' } } |
| $gt | Greater than | { price: { $gt: 100 } } |
| $gte | Greater than or equal to | { age: { $gte: 18 } } |
| $lt | Less than | { stock: { $lt: 10 } } |
| $lte | Less than or equal to | { score: { $lte: 50 } } |
| $in | Value is in the array | { role: { $in: ['admin', 'mod'] } } |
| $nin | Value is NOT in the array | { status: { $nin: ['banned', 'deleted'] } } |
| $contains | String contains substring (case-insensitive) | { name: { $contains: 'ali' } } |
| $startsWith | String starts with prefix (case-insensitive) | { email: { $startsWith: 'admin' } } |
| $endsWith | String ends with suffix (case-insensitive) | { email: { $endsWith: '.gov' } } |
| $regex | String matches regexp (case-insensitive) | { phone: { $regex: '^\\+1' } } |
| $exists | Field exists (true) or is null/undefined (false) | { avatar: { $exists: true } } |
| $type | typeof field equals the given string | { score: { $type: 'number' } } |
| $size | Array field has exactly N elements | { tags: { $size: 3 } } |
Dot Notation
Access nested fields with dot notation:
{ 'address.city': 'Berlin' }
{ 'meta.score': { $gte: 90 } }Function Predicate
Pass a function for full programmatic control:
const { data } = await ark.findAll('users', {
filter: r => r.tags?.includes('vip') && r.age > 25
});Patch Operators
Used with ark.patch() for atomic field-level mutations.
| Operator | Value Type | Description |
|---|---|---|
| $set | { field: value } | Set field to a value |
| $unset | ['field', ...] | Delete (remove) the listed fields |
| $inc | { field: n } | Add n to the numeric field |
| $dec | { field: n } | Subtract n from the numeric field |
| $mul | { field: n } | Multiply the numeric field by n |
| $toggle | { field: true } | Flip a boolean field to its opposite |
| $push | { field: value } | Append value to an array field |
| $pull | { field: value } | Remove all occurrences of value from an array field |
Full Method List
| Method | Returns | Description |
|---|---|---|
| open(name, version, schema) | Promise<this> | Open or create database |
| close() | void | Close connection |
| ArkIndexDb.dropDatabase(name) | Promise<boolean> | Delete a database (static) |
| ArkIndexDb.listDatabases() | Promise<Array> | List origin databases (static) |
| insert(store, data) | Promise<key> | Insert one record |
| insertMany(store, records[]) | Promise<key[]> | Batch insert in one transaction |
| findById(store, id) | Promise<Object\|null> | Fetch by primary key |
| findAll(store, options?) | Promise<{data, total}> | Fetch with filter/sort/page |
| findByIndex(store, index, value) | Promise<{data, total}> | Native IDB index lookup |
| findByRange(store, index, range) | Promise<{data, total}> | Key range query on index |
| findOne(store, filter) | Promise<Object\|null> | First match or null |
| exists(store, id) | Promise<boolean> | Check record existence |
| count(store, filter?) | Promise<number> | Count records |
| getAllKeys(store) | Promise<Array> | All primary keys |
| paginate(store, page, size, opts?) | Promise<PaginationResult> | Paginated fetch |
| update(store, id, changes) | Promise<Object> | Partial record merge |
| upsert(store, data) | Promise<key> | Insert or replace |
| patch(store, id, ops) | Promise<Object> | Field-level patch operators |
| updateWhere(store, filter, changes) | Promise<Object[]> | Bulk update by filter |
| delete(store, id) | Promise<boolean> | Delete by primary key |
| deleteWhere(store, filter) | Promise<number> | Delete all matching |
| clear(store) | Promise<number> | Delete all records in store |
| query(store) | ArkQueryBuilder | Get fluent query builder |
| transaction(stores[], mode, fn) | Promise<void> | Multi-store atomic transaction |
| iterateCursor(store, cb, dir?) | Promise<void> | Memory-efficient cursor iteration |
| exportStore(store) | Promise<string> | Export store as JSON string |
| exportAll() | Promise<string> | Export all stores as JSON |
| importStore(store, json) | Promise<number> | Import records from JSON |
| downloadStore(store) | Promise<void> | Trigger browser file download |
| on(event, callback) | Function (unsubscribe) | Subscribe to an event |
| off(event, callback) | void | Unsubscribe a listener |
Files
| File | Description |
|---|---|
| ark-indexdb.js | The standalone library — include this in any project |
| demo.html | Interactive demo + full documentation viewer |
| README.md | This file |
Usage with the HTML demo
Open demo.html in a browser. It loads ark-indexdb.js from the same directory automatically (with a built-in inline fallback so it also works as a standalone single file).
The demo has two views:
- ⬡ Live Database — interact with a real IndexedDB in your browser with 3 seeded object stores, full CRUD UI, search, filters, pagination, and an operation log
- ◈ Documentation — the full Getting Started guide, all API docs, and interactive code blocks with copy buttons
License
MIT License © 2024 ArkIndexDb Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
