@kyusu0918/ofc-indexeddb
v1.1.1
Published
A type-safe IndexedDB wrapper for TypeScript that provides typed store factories, simple CRUD methods and built-in logical delete support.
Maintainers
Readme
🗂️ ofc-indexeddb
Type-safe, zero-dependency IndexedDB wrapper for TypeScript with logical delete, async CRUD, and typed store factories. by Kei Yusu (Oresama Foundation Code)
📦 Overview
ofc-indexeddb is a lightweight IndexedDB wrapper written in TypeScript,
offering a unified, type-safe, and promise-based interface for database operations.
It's part of the Oresama Foundation Code (ofc) series — focusing on practical utilities with consistent design, high readability, and zero external dependencies.
⚙️ Features
- ✅ Type-safe CRUD operations for IndexedDB.
- 🧩 Promise-based async API.
- 🧠 Logical deletion (soft delete with
is_delete+ timestamp). - 🧱 Schema factory via
bindStore()for reusable, typed shortcuts. - 🧮 Built-in date (
now) and ID auto-generation (crypto.randomUUID). - 🔒 Zero dependency, minimal footprint.
- 🧪 Fully covered by Jest tests (96%+ line coverage).
💡 Why ofc-indexeddb?
A next-generation IndexedDB wrapper for developers who demand both type-safety and simplicity.
Unlike most IndexedDB wrappers, ofc-indexeddb is not just a Promise wrapper.
It’s a fully type-safe, functionally pure API that lets you express any filter logic — including regex, date range, and complex predicates — directly in TypeScript. Traditional libraries often make you adapt to their APIs; ofc-indexeddb lets you use JavaScript itself as your query language.
🔍 Feature Comparison
| Feature | Dexie.js | idb | localforage | ofc-indexeddb |
| :--- | :--- | :--- | :--- | :--- |
| TypeScript Support | ⚠️ Partial | ❌ Minimal | ❌ None | ✅ Full generics |
| Logical Delete Built-in | ❌ | ❌ | ❌ | ✅ Yes |
| Regex / Function Filters | ❌ | ⚠️ Manual | ❌ | ✅ Inline ((r) => /foo/.test(r.name)) |
| Proxy-safe (Vue/React) | ❌ | ❌ | ❌ | ✅ isProxy option |
| Pure Functional API | ⚠️ Mixed | ✅ | ❌ | ✅ 100% |
| DB Creation Syntax | Imperative | Native | Hidden | ✅ Declarative createStore() |
| Dependencies | Medium | Low | Medium | Zero |
🧠 Type-Safe by Design
All operations are typed at compile time. If you access an unknown property or mismatched type, TypeScript catches it.
// ❌ Compile-time error (nonexistent field)
await Users.select(db, r => r.unknownField === 1);
// ✅ Valid, fully typed
await Users.select(db, r => /^[A-Z]/.test(r.name) && r.age >= 18);This means ofc-indexeddb achieves practical type safety
that no other IndexedDB library has reached yet.
💬 Why It Matters
Most IndexedDB wrappers wrap complexity. ofc-indexeddb eliminates it — without losing type safety.
You can think of it as:
🧩 “Dexie meets TypeScript generics and functional purity.”**
No custom query syntax. No hidden ORM layer.
Just type-safe JavaScript that works in any browser or Node environment.
🧪 Philosophy
“Simplicity is not the absence of power — it’s the absence of noise.”
ofc-indexeddb is designed for developers who want a predictable, zero-dependency, and composable way to work with browser storage without magic or boilerplate.
⚡ Example Power Queries
// Regex + logic combo
const results = await Users.select(db, r =>
/^[AB]/.test(r.name) && r.age >= 18 && !r.is_delete
);
// Date range filtering
const recent = await Sessions.select(db, r =>
new Date(r.timestamp) > new Date("2025-01-01")
);
// Complex logical filters
const activeTitles = await Titles.select(db, r =>
!r.is_delete && /Project/i.test(r.title) && r.user_id === user.id
);📘 Installation
npm install @kyusu0918/ofc-indexeddb🚀 Usage Example
1️⃣ Define Data Models & Schema
All data models must extend OfcRec to inherit metadata fields (id, inserted, updated, deleted, is_delete).
import ofcIndexedDB, { OfcRec } from '@kyusu0918/ofc-indexeddb';
// Define Data Model (must extend OfcRec)
interface iUser extends OfcRec {
name: string;
age: number;
}
// Define Schema Creation Function
export const createStoreV1 = (db: IDBDatabase) => {
// Create 'users' store with 'id' as primary key
const users = db.createObjectStore('users', { keyPath: 'id' });
users.createIndex('name', 'name', { unique: false });
};2️⃣ Recommended Usage: Type-Safe CRUD with DB-Bound Store (bindStore)
When integrating with application frameworks (like React or Vue) where you want the store layer to manage the db instance, using bindStore() to bind the db instance to the store object is ideal. This eliminates the need to pass the db argument in subsequent CRUD operations.
// 1. Establish DB connection (Version 1)
const db = await ofcIndexedDB.connect('AppDB', 1, createStoreV1);
// 2. Bind the DB instance and define the store shortcut
// Using bindStore() causes the generated object (Users) to hold the db instance.
const Users = ofcIndexedDB.bindStore<iUser>(db, 'users', {
logicalDelete: true, // Users.delete() will default to soft delete
});
/**
* 1️⃣ Insert a new user and get the generated ID
* NOTE: Users.upsert() automatically generates ID and timestamps.
* **No db argument is required.**
*/
const newUserId = await Users.upsert({
name: 'Alice',
age: 25,
} as iUser); // Use 'as iUser' since required base fields are auto-generated
/**
* 2️⃣ Retrieve and Update the record
* The `updated` timestamp is automatically refreshed upon save.
*/
let user = await Users.get(newUserId);
user.age = 26;
await Users.upsert(user); // **No db argument required**
/**
* 3️⃣ Filter (WHERE equivalent)
* select() retrieves non-deleted records by default.
*/
const adults = await Users.select(
(r) => r.age >= 20 // Filters for users older than 20
); // **No db argument required**
console.log(adults);
/**
* 4️⃣ Logical Delete (Soft Delete)
* Marks a record as deleted (is_delete = true), updating `deleted` and `updated` timestamps.
*/
await Users.delete(newUserId); // **No db argument required**
/**
* 5️⃣ Verify Logical Deletion
*/
const deletedUser = await Users.get(newUserId);
console.log(deletedUser.is_delete); // true3️⃣ Unconstrained Query: The Power of select()
(This section is separated to keep the full CRUD flow clean, but demonstrates the true power)
// Select only records where name starts with 'B'.
const activeUsers = await Users.select((r) => r.name.startsWith('B'));
// Regex + logic combo: Complex search using Regex, age, and manual logical delete check
const results = await Users.select(r =>
/^[AB]/.test(r.name) && r.age >= 18 && !r.is_delete
);🧩 Core Methods: Native CRUD Example
For advanced users who prefer explicit control over store operations, the base methods remain fully accessible.
This example demonstrates the core functionality of ofcIndexedDB using native methods, showing how to handle data with explicit store names.
// 1. Insert (Simple, typed call) - Explicitly pass the store name 'users'
const newUserId = await ofcIndexedDB.upsert<iUser>(db, 'users', {
name: 'Bob',
age: 33,
} as iUser);
// 2. Logical Delete - Must explicitly pass the logical option
await ofcIndexedDB.delete(db, 'users', newUserId, { logical: true });
// 3. Select (Filter) - Must explicitly exclude deleted records if logical delete was used
const activeUsers = await ofcIndexedDB.select<iUser>(db, 'users', (r) =>
r.name.startsWith('B') && !r.is_delete
);
console.log(activeUsers);Error Handling
All CRUD methods return a Promise and throw Error objects when IndexedDB operations fail.
Always wrap calls in try...catch when necessary.
try {
const user = await ofcIndexedDB.get(db, "users", "001");
} catch (err) {
console.error("Database error:", err);
}All methods throw native
Errorobjects and never silently fail. You always get clear exception messages for easier debugging.
🧱 API Overview
| Method | Description | Example |
| :--- | :--- | :--- |
| connect(name, version, createFunc) | Opens or creates a DB. | await connect('MyDB', 1, initFn) |
| get(db, store, key, index?) | Retrieve one record by key or index. | get(db, 'users', 'id-001') |
| list(db, store, index?, from?, to?) | Retrieve multiple records by range. | list(db, 'users') |
| select(db, store, whereFn, options?) | Filter records by condition (WHERE equivalent). | select(db, 'users', r => r.age > 30) |
| upsert(db, store, rec, options?) | Insert or update record. | upsert(db, 'users', rec) |
| delete(db, store, key, options?) | Perform logical (soft) or physical delete. | delete(db, 'users', id, { logical: true }) |
| clear(db, store) | Truncate all records from the store. | clear(db, 'users') |
| defineStore(store, defaults?) | [Recommended] Typed CRUD wrapper factory. | const s = defineStore('users') |
🧪 Testing
All tests are passing thanks to the recent fixes.
Run all tests (with Jest + fake-indexeddb):
npm testExample result (indicating full functionality and high coverage):
PASS tests/ofcIndexedDB.test.ts
Tests: 25 passed, 25 total
-----------------|---------|----------|---------|---------
File | % Stmts | % Branch | % Funcs | % Lines
ofcIndexedDB.ts | 87.65 | 82.14 | 76.92 | 95.55📁 Project Structure
ofc-indexeddb/
├── src/
│ └── ofcIndexedDB.ts
├── tests/
│ └── ofcIndexedDB.test.ts
├── dist/
│ ├── ofcIndexedDB.js
│ ├── ofcIndexedDB.d.ts
│ └── ofcIndexedDB.js.map
├── package.json
├── tsconfig.json
└── jest.config.js📘 Documentation
- API Reference (English)
- APIリファレンス(日本語)
- React & Vue 3 Integration Guide (English)
- React & Vue 3 統合ガイド (日本語)
🔒 License
MIT © Kei Yusu Part of the Oresama Foundation Code series.
[1.1.0] - 2025-11-11
Added
- Added
close()to safely close an IndexedDB connection. - Full test coverage for success, null, and error cases.
[1.1.1] - 2025-11-12
Changed
- defineStore() now defaults logicalDelete to false when the option is omitted.
“Typed. Simple. Persistent. — A new standard for type-safe browser storage.”
