{"id":31758388,"url":"https://github.com/joe960913/kengo","last_synced_at":"2025-10-26T12:05:01.170Z","repository":{"id":311517619,"uuid":"1043921598","full_name":"joe960913/kengo","owner":"joe960913","description":"A Prisma-like, type-safe, and reactive ORM for IndexedDB. Say goodbye to callbacks and enjoy a modern developer experience in the browser.","archived":false,"fork":false,"pushed_at":"2025-09-15T05:20:15.000Z","size":816,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-30T00:59:46.007Z","etag":null,"topics":["browsers","client-side","databases","drizzle","indexed-db","indexeddb-wrapper","kengo","localstorage","orms","prisma-orm","pwa","reactive","storage","type-safe","typescipt"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/kengo","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joe960913.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-24T22:35:35.000Z","updated_at":"2025-09-10T10:01:23.000Z","dependencies_parsed_at":"2025-08-25T02:15:03.829Z","dependency_job_id":null,"html_url":"https://github.com/joe960913/kengo","commit_stats":null,"previous_names":["joe960913/kengo"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/joe960913/kengo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joe960913%2Fkengo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joe960913%2Fkengo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joe960913%2Fkengo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joe960913%2Fkengo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joe960913","download_url":"https://codeload.github.com/joe960913/kengo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joe960913%2Fkengo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002014,"owners_count":26083258,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["browsers","client-side","databases","drizzle","indexed-db","indexeddb-wrapper","kengo","localstorage","orms","prisma-orm","pwa","reactive","storage","type-safe","typescipt"],"created_at":"2025-10-09T20:25:01.208Z","updated_at":"2025-10-09T20:25:03.278Z","avatar_url":"https://github.com/joe960913.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \n  # ⚔︎\n  \n  # **K E N G O**\n  \n  ### 剣豪\n  \n  \u003cbr /\u003e\n  \n  **A modern, type-safe, and reactive ORM for IndexedDB**  \n  *Wield the power of a server-side ORM, directly in the browser*\n  \n  \u003cbr /\u003e\n  \n  [![NPM Version](https://img.shields.io/npm/v/kengo?style=for-the-badge\u0026labelColor=1a1b26\u0026color=7aa2f7\u0026logo=npm)](https://www.npmjs.com/package/kengo) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/kengo?style=for-the-badge\u0026labelColor=1a1b26\u0026color=9ece6a\u0026logo=javascript)](https://bundlephobia.com/result?p=kengo) [![Tests](https://img.shields.io/github/actions/workflow/status/joe960913/kengo/test.yml?branch=main\u0026label=tests\u0026style=for-the-badge\u0026labelColor=1a1b26\u0026color=bb9af7\u0026logo=github)](https://github.com/joe960913/kengo/actions) [![License](https://img.shields.io/npm/l/kengo?style=for-the-badge\u0026labelColor=1a1b26\u0026color=e0af68\u0026logo=opensourceinitiative)](https://opensource.org/licenses/MIT)  \n  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-7aa2f7?style=for-the-badge\u0026labelColor=1a1b26\u0026logo=typescript)](https://www.typescriptlang.org/) [![IndexedDB](https://img.shields.io/badge/IndexedDB-Powered-f7768e?style=for-the-badge\u0026labelColor=1a1b26\u0026logo=database)](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) [![Zero Deps](https://img.shields.io/badge/Zero-Dependencies-9ece6a?style=for-the-badge\u0026labelColor=1a1b26)](https://www.npmjs.com/package/kengo?activeTab=dependencies) [![Browser](https://img.shields.io/badge/Browser-Compatible-73daca?style=for-the-badge\u0026labelColor=1a1b26\u0026logo=googlechrome)](https://caniuse.com/indexeddb)\n  \n  \u003cbr /\u003e\n  \n  ---\n  \n\u003c/div\u003e\n\nManaging data in the browser shouldn't feel like a clumsy battle. IndexedDB is powerful, but its native API is a maze of callbacks and verbose boilerplate—like wielding a blunt, heavy sword in the fog.\n\n**Kengo** brings the art of the swordsman—**mastery, precision, and elegance**—to this battlefield. Inspired by the masterful developer experience of Prisma, Kengo allows you to command your browser's database with the confidence and clarity of a _Kengo_ (a master swordsman).\n\n---\n\n## The Way of Kengo - 剣豪之道\n\n- ⚔️ **Schema First (The Form - 型):** Define your data structure in a single, readable schema file. This is your _kata_—the source of truth from which all data interactions flow with discipline and purpose.\n- ✨ **Fully Type-Safe (The Blade - 刃):** Wield an auto-generated, fully-typed client. Your editor becomes your whetstone, sharpening your code and eliminating errors at compile time for a flawless edge.\n- 🌊 **Fluent \u0026 Familiar API (The Stance - 構え):** Enjoy a modern, chainable API that feels just like Prisma. Every query is an elegant, powerful stance, allowing you to strike with precision and grace.\n- 🚀 **Reactive \u0026 Real-time (The Mind - 心):** Go beyond simple CRUD. With `liveQuery`, your UI can subscribe to data changes and update automatically. Your application becomes a living entity, aware and responsive, like a swordsman in a state of total concentration.\n- 🛡️ **Zero-Config Migrations (The Scabbard - 鞘):** Evolve your schema without fear. Simply increment your version number, and Kengo handles the migration automatically. Your blade is always ready, seamlessly adapting to new challenges.\n\n## Quick Start: The First Cut\n\nExperience the power of Kengo in minutes.\n\n```bash\n# npm\nnpm install kengo\n\n# yarn\nyarn add kengo\n\n# pnpm\npnpm add kengo\n\n# bun\nbun add kengo\n```\n\nDefine your schema, forge your client, and wield the blade.\n\n```typescript\nimport { defineSchema, Kengo } from 'kengo'\n\n// 1. Define your schema (The Form)\nconst schema = defineSchema({\n  version: 1,\n  stores: {\n    users: {\n      '@@id': { keyPath: 'id', autoIncrement: true },\n      '@@uniqueIndexes': ['email'],\n      '@@indexes': ['age'],\n    },\n  },\n})\n\n// 2. Forge your client\nconst db = new Kengo({ name: 'my-app-db', schema })\n\n// 3. Wield the blade with a familiar, type-safe API!\nasync function main() {\n  const newUser = await db.users.create({\n    data: { name: 'Musashi', email: 'musashi@kengo.io', age: 29 },\n  })\n\n  const skilledSwordsmen = await db.users.findMany({\n    where: { age: { gte: 25 } },\n    orderBy: { name: 'asc' },\n    take: 10,\n  })\n\n  console.log('Found:', skilledSwordsmen)\n}\n\nmain()\n```\n\n## Comparison: Kengo vs. The Alternatives\n\n| Feature              | **Kengo (The Master's Blade)**         | Native IndexedDB (Raw Iron)         | localStorage (Swiss Army Knife) |\n| -------------------- | -------------------------------------- | ----------------------------------- | ------------------------------- |\n| **API**              | ✅ Async/Await, Fluent, Prisma-like    | ❌ Callback-based, verbose, complex | ❌ Synchronous, blocking, basic |\n| **Type Safety**      | ✅ Fully Type-Safe, Auto-generated     | ❌ `any`, error-prone               | ❌ String-only, manual parsing  |\n| **Querying**         | ✅ Powerful, indexed, multi-faceted    | ❌ Manual cursors \u0026 ranges          | ❌ Key-only, no indexing        |\n| **Migrations**       | ✅ Automatic, safe, version-controlled | ❌ Manual, high-risk, boilerplate   | ❌ N/A                          |\n| **Storage Capacity** | ✅ Very Large (GBs+)                   | ✅ Very Large (GBs+)                | ❌ Tiny (~5MB)                  |\n| **Reactivity**       | ✅ **Built-in `liveQuery`**            | ❌ None, requires manual polling    | ❌ `storage` event (limited)    |\n\n## The Roadmap: The Path to Mastery\n\nKengo is an actively developed project on a mission to deliver the ultimate browser database experience. Here's what the future holds:\n\n- [x] **Core:** Rock-solid, type-safe CRUD \u0026 Query Engine.\n- [x] **Migrations:** Automatic schema migrations.\n- [ ] **Reactivity:** `liveQuery` API for real-time UI updates.\n- [ ] **Relations:** Simple, Prisma-like relation queries (`include`, `select`).\n- [ ] **Full-Text Search:** Integrated full-text search capabilities.\n- [ ] **Data Seeding:** A dedicated API for seeding development data.\n- [ ] **Kengo Studio:** A developer tool for visualizing and managing your local database.\n\n## 📚 Complete API Reference\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cstrong\u003eClick to expand the full API documentation\u003c/strong\u003e\u003c/summary\u003e\n\n### 🏗️ Schema Definition\n\nDefine your database structure with a type-safe schema.\n\n```typescript\nconst schema = defineSchema({\n  version: 1, // Increment to trigger migrations\n  stores: {\n    users: {\n      '@@id': { keyPath: 'id', autoIncrement: true }, // Primary key\n      '@@indexes': ['email', 'age', 'country'], // Queryable indexes\n      '@@uniqueIndexes': ['email'], // Unique constraints\n    },\n  },\n})\n```\n\n**Schema Options:**\n\n- `version`: Database version (positive integer)\n- `stores`: Object defining your tables/stores\n- `@@id`: Primary key configuration\n  - String: `'@@id': 'userId'` (simple key path)\n  - Object: `{ keyPath: 'id', autoIncrement: true }` (auto-increment)\n- `@@indexes`: Array of field names for non-unique indexes\n- `@@uniqueIndexes`: Array of field names for unique indexes\n\n---\n\n### ✨ CRUD Operations\n\n#### **Create Operations**\n\n```typescript\n// Create single record\nconst user = await db.users.create({\n  data: { name: 'Alice', email: 'alice@example.com', age: 25 },\n  select: { id: true, name: true }, // Optional: Return specific fields\n})\n\n// Create multiple records\nconst result = await db.users.createMany({\n  data: [\n    { name: 'Bob', email: 'bob@example.com', age: 30 },\n    { name: 'Charlie', email: 'charlie@example.com', age: 35 },\n  ],\n  skipDuplicates: true, // Skip records that violate unique constraints\n})\n// Returns: { count: 2 }\n```\n\n#### **Read Operations**\n\n```typescript\n// Find by unique field (returns null if not found)\nconst user = await db.users.findUnique({\n  where: { id: 1 }, // or { email: 'alice@example.com' }\n  select: { name: true, email: true },\n})\n\n// Find first matching record (returns null if not found)\nconst firstUser = await db.users.findFirst({\n  where: { age: { gte: 25 } },\n  orderBy: { createdAt: 'desc' },\n})\n\n// Find multiple records\nconst users = await db.users.findMany({\n  where: {\n    age: { gte: 18, lte: 65 },\n    country: 'US',\n  },\n  orderBy: [{ age: 'desc' }, { name: 'asc' }],\n  skip: 10,\n  take: 20,\n  select: { id: true, name: true, email: true },\n})\n\n// Count records\nconst count = await db.users.count({\n  where: { age: { gte: 18 } },\n})\n```\n\n#### **Update Operations**\n\n```typescript\n// Update single record\nconst updated = await db.users.update({\n  where: { id: 1 },\n  data: {\n    name: 'Alice Smith',\n    age: { increment: 1 }, // Atomic operation\n  },\n  select: { id: true, name: true, age: true },\n})\n\n// Update multiple records\nconst result = await db.users.updateMany({\n  where: { country: 'US' },\n  data: {\n    isActive: true,\n    credits: { multiply: 1.1 }, // Give 10% bonus\n  },\n})\n// Returns: { count: 42 }\n```\n\n#### **Delete Operations**\n\n```typescript\n// Delete single record (throws if not found)\nconst deleted = await db.users.delete({\n  where: { id: 1 },\n})\n\n// Delete multiple records\nconst result = await db.users.deleteMany({\n  where: { age: { lt: 18 } },\n})\n// Returns: { count: 5 }\n```\n\n#### **Upsert Operation**\n\n```typescript\n// Create if not exists, update if exists\nconst user = await db.users.upsert({\n  where: { email: 'alice@example.com' },\n  create: {\n    name: 'Alice',\n    email: 'alice@example.com',\n    age: 25,\n  },\n  update: {\n    age: { increment: 1 },\n    lastSeen: new Date(),\n  },\n  select: { id: true, name: true, age: true },\n})\n```\n\n---\n\n### 🔍 Query Conditions\n\n#### **Where Conditions**\n\n```typescript\n// Equality (implicit)\nwhere: { name: 'Alice' }\n\n// Equality (explicit)\nwhere: { name: { equals: 'Alice' } }\n\n// Not equal\nwhere: { status: { not: 'deleted' } }\n\n// In array\nwhere: { role: { in: ['admin', 'moderator'] } }\n\n// Not in array\nwhere: { status: { notIn: ['deleted', 'suspended'] } }\n\n// Greater than / Greater than or equal\nwhere: { age: { gt: 18 } }\nwhere: { age: { gte: 18 } }\n\n// Less than / Less than or equal\nwhere: { price: { lt: 100 } }\nwhere: { price: { lte: 100 } }\n\n// String operations\nwhere: { email: { contains: '@gmail.com' } }\nwhere: { name: { startsWith: 'John' } }\nwhere: { url: { endsWith: '.com' } }\n\n// Combine multiple conditions (AND)\nwhere: {\n  age: { gte: 18, lte: 65 },\n  country: 'US',\n  isActive: true\n}\n```\n\n#### **Ordering**\n\n```typescript\n// Single field\norderBy: {\n  createdAt: 'desc'\n}\n\n// Multiple fields\norderBy: [{ category: 'asc' }, { price: 'desc' }]\n```\n\n#### **Pagination**\n\n```typescript\n// Skip and take\n{\n  skip: 20,   // Skip first 20 records\n  take: 10    // Take next 10 records\n}\n```\n\n#### **Field Selection**\n\n```typescript\n// Select specific fields\nselect: {\n  id: true,\n  name: true,\n  email: true\n  // Other fields will not be returned\n}\n```\n\n---\n\n### ⚛️ Atomic Operations\n\nPerform atomic numeric operations without race conditions.\n\n```typescript\n// Increment\nawait db.counters.update({\n  where: { id: 1 },\n  data: { value: { increment: 5 } },\n})\n\n// Decrement\nawait db.counters.update({\n  where: { id: 1 },\n  data: { value: { decrement: 3 } },\n})\n\n// Multiply\nawait db.counters.update({\n  where: { id: 1 },\n  data: { value: { multiply: 2 } },\n})\n\n// Divide (ignores division by zero)\nawait db.counters.update({\n  where: { id: 1 },\n  data: { value: { divide: 4 } },\n})\n\n// Combine with regular updates\nawait db.users.update({\n  where: { id: 1 },\n  data: {\n    name: 'Updated Name',\n    points: { increment: 100 },\n    multiplier: { multiply: 1.5 },\n  },\n})\n```\n\n---\n\n### 🔄 Transactions\n\nEnsure data consistency with ACID transactions.\n\n```typescript\n// Basic transaction\nconst result = await db.$transaction(async (tx) =\u003e {\n  // All operations use 'tx' instead of 'db'\n  const user = await tx.users.create({\n    data: { name: 'Alice', email: 'alice@example.com' },\n  })\n\n  const post = await tx.posts.create({\n    data: {\n      userId: user.id,\n      title: 'First Post',\n      content: 'Hello World!',\n    },\n  })\n\n  await tx.users.update({\n    where: { id: user.id },\n    data: { postCount: { increment: 1 } },\n  })\n\n  return { user, post } // Return value from transaction\n})\n\n// Transaction with error handling\ntry {\n  await db.$transaction(async (tx) =\u003e {\n    await tx.accounts.update({\n      where: { id: senderId },\n      data: { balance: { decrement: amount } },\n    })\n\n    if (amount \u003e 1000) {\n      throw new Error('Amount too large!') // Rollback\n    }\n\n    await tx.accounts.update({\n      where: { id: receiverId },\n      data: { balance: { increment: amount } },\n    })\n  })\n} catch (error) {\n  // Transaction rolled back, no changes applied\n  console.error('Transaction failed:', error)\n}\n```\n\n**Transaction Rules:**\n\n- All operations within a transaction are atomic\n- If any operation fails, all changes are rolled back\n- Nested transactions are not supported\n- Cannot call `$disconnect()` within a transaction\n- Can access raw database with `tx.$getRawDB()`\n\n---\n\n### 🔄 Automatic Migrations\n\nKengo handles schema migrations automatically when you increment the version number.\n\n```typescript\n// Version 1: Initial schema\nconst schemaV1 = defineSchema({\n  version: 1,\n  stores: {\n    users: {\n      '@@id': { keyPath: 'id', autoIncrement: true },\n      '@@indexes': ['email'],\n    },\n  },\n})\n\n// Version 2: Add new store and indexes\nconst schemaV2 = defineSchema({\n  version: 2, // Increment version to trigger migration\n  stores: {\n    users: {\n      '@@id': { keyPath: 'id', autoIncrement: true },\n      '@@indexes': ['email', 'createdAt'], // Added new index\n      '@@uniqueIndexes': ['username'], // Added unique constraint\n    },\n    posts: {\n      // New store added\n      '@@id': { keyPath: 'id', autoIncrement: true },\n      '@@indexes': ['userId', 'publishedAt'],\n    },\n  },\n})\n\n// Kengo automatically:\n// 1. Detects version change (1 → 2)\n// 2. Creates new stores (posts)\n// 3. Adds new indexes (createdAt, username)\n// 4. Preserves all existing data\n// 5. Handles the migration safely\nconst db = new Kengo({\n  name: 'my-app',\n  schema: schemaV2, // Just use the new schema!\n})\n```\n\n**Migration Features:**\n\n- **Zero-config**: Just increment the version number\n- **Non-destructive**: Existing data is always preserved\n- **Additive changes**: Add new stores, indexes, and unique constraints\n- **Automatic handling**: No migration files or scripts needed\n- **Safe rollback**: Old app versions continue to work with their schema version\n\n**Important Notes:**\n\n- Always increment the version number when changing schema\n- You cannot remove stores or indexes (IndexedDB limitation)\n- Schema changes are applied when the database is first opened\n- Each browser profile maintains its own schema version\n\n---\n\n### 🔧 Advanced Features\n\n#### **Raw Database Access**\n\n```typescript\n// Get the underlying IDBDatabase instance\nconst rawDB = await db.$getRawDB()\n\n// Use native IndexedDB API for advanced operations\nconst transaction = rawDB.transaction(['users'], 'readonly')\nconst objectStore = transaction.objectStore('users')\nconst index = objectStore.index('email')\n// ... perform native IndexedDB operations\n```\n\n#### **Connection Management**\n\n```typescript\n// Initialize database (called automatically on first operation)\nawait db.$connect()\n\n// Close database connection\nawait db.$disconnect()\n\n// Check connection status\nconst isConnected = db.$isConnected()\n```\n\n---\n\n### 📝 TypeScript Support\n\nKengo provides full TypeScript support with auto-generated types.\n\n```typescript\nimport { defineSchema, Kengo } from 'kengo'\n\n// Define your data types\ninterface User {\n  id?: number\n  name: string\n  email: string\n  age: number\n  createdAt: Date\n}\n\n// Schema is fully typed\nconst schema = defineSchema({\n  version: 1,\n  stores: {\n    users: {\n      '@@id': { keyPath: 'id', autoIncrement: true },\n      '@@uniqueIndexes': ['email'],\n    },\n  },\n})\n\n// Client is fully typed based on schema\nconst db = new Kengo({ name: 'my-app', schema })\n\n// All operations are type-safe\nconst user = await db.users.create({\n  data: {\n    name: 'Alice', // ✅ Required\n    email: 'alice@example.com', // ✅ Required\n    age: 25, // ✅ Required\n    // id is optional (auto-increment)\n    // unknown: 'field'  // ❌ TypeScript error\n  },\n})\n```\n\n---\n\n### 🚀 Performance Tips\n\n1. **Use Indexes**: Define indexes for fields you query frequently\n2. **Select Fields**: Use `select` to return only needed fields\n3. **Batch Operations**: Use `createMany`, `updateMany`, `deleteMany` for bulk operations\n4. **Transactions**: Group related operations in transactions\n5. **Pagination**: Use `skip` and `take` for large datasets\n\n\u003c/details\u003e\n\n## Contributing\n\nKengo's path to mastery is forged by its community. Contributions of all forms are welcome, from bug reports to new features. Please see our **[Contributing Guide](CONTRIBUTING.md)** to get started.\n\n## License\n\nLicensed under the **[MIT License](LICENSE)**.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoe960913%2Fkengo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoe960913%2Fkengo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoe960913%2Fkengo/lists"}