bxd

BoxDB is a promise-based browser ORM for IndexedDB

Concept

IndexedDB is async, fast, powerful. but not support Promise and must use callback pattern to using IndexedDB. It feels like a legacy code.

For this reason, BoxDB project has started.

But, why name is BoxDB?

  • Boxes are lightweight.
  • Boxes has shape.
  • Boxes can store something.
  • Boxes are easy to take.

This is the starting point and core concept of BoxDB.

Boxes are the essence of BoxDB. A box is an abstraction that represents a object store in your IndexedDB.

Box is the same concept like Model used by other ORMs.

Features

  • Promise based ORM
  • User friendly and easy to use
  • Lightweight(< 10kb) IndexedDB wrapper
  • Zero dependency
  • Database and object store version management
  • Data validation and transaction control via model (box)
  • ACID(Atomicity, Consistency, Isolation, Durability) guaranteed with transaction
  • Supports TypeScript
  • Works on Web workers

Browser Support

ie IE edge Edge firefox Firefox chrome Chrome safari Safari ios-safari iOS Safari samsung Samsung opera Opera
11 12~ 10~ 23~ 10~ 10~ 4~ 15~
  • Test features in your browser here.
  • Checkout IE11 test here.

Installation

BoxDB is available via npm (or yarn).

1
2
3
yarn add bxd
# or
npm install bxd

Basics

Preparing for database

If database is not exist, create new one.

1
2
3
import BoxDB from 'bxd';

const db = new BoxDB('database-name', 1); // database name, version

Create box

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// User Box (object store name: user)
const User = db.create('user', {
  _id: {
    type: BoxDB.Types.STRING,
    key: true,
  },
  name: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  age: BoxDB.Types.NUMBER,
});

// Item Box (object store name: item)
const Item = db.create('item', {
  uid: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  name: {
    type: BoxDB.Types.STRING,
    index: true,
  },
  memo: BoxDB.Types.STRING,
});

Update database version

If you want change database version (eg. change box schema, create new box, etc) just change to larger number then previous version

1
2
3
4
// Previous version
// const db = new BoxDB('database-name', 1);

const db = new BoxDB('database-name', 2); // Just like this!

Open database

1
2
3
4
5
6
// define boxes
// ...

await db.open();

// You can use Boxes from here! (data access/control)

Close database

It usually used in special cases. (eg. open database that has same name)

1
db.close();

Transaction

Add single record

1
2
await User.add({ _id: '0', name: 'EMPTY', age: 0 });
await User.add({ _id: '1', name: 'leegeunhyeok', age: 20 });

Add multiple records

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const items = [
  { uid: '0', name: 'unknown 1', memo: '' },
  { uid: '0', name: 'unknown 2', memo: '' },
  { uid: '1', name: 'mac', memo: 'so expensive' },
  { uid: '1', name: 'phone', memo: '' },
  { uid: '1', name: 'desktop', memo: '' },
];

// In async function
for (let i = 0; i < items.length; i++) {
  await Item.add(items[i]);
}

Get record

1
2
await User.get('1'); // { _id: '1', name: 'leegeunhyeok', age: 20 }
await User.get('999'); // null

Update record

1
await User.put({ _id: '0', name: 'temp' }); // Update `user._id = '0'`: `name` will update to 'temp'

Delete record

1
await User.delete('0'); // Delete `user._id = '0'`

Get all records by cursor

1
2
3
4
5
6
7
8
9
await Item.find().get();

// [
//   { uid: '0', name: 'unknown 1', memo: '' },
//   { uid: '0', name: 'unknown 2', memo: '' },
//   { uid: '1', name: 'mac', memo: 'so expensive' },
//   { uid: '1', name: 'phone', memo: '' },
//   { uid: '1', name: 'desktop', memo: '' }
// ]

Get records by cursor (using index)

1
2
3
await Item.find({ index: 'name', value: 'phone' }).get();

// [{ uid: '1', name: 'phone', memo: '' }]

Get records by cursor (using filter functions)

1
2
3
4
5
6
7
await Item.find(
  null,
  (item) => parseInt(item.uid) % 2 === 1,
  (item) => item.memo.includes('expensive'),
).get();

// [{ uid: '1', name: 'mac', memo: 'so expensive' }]

Get records by cursor (both)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
await Item.find(
  {
    index: 'uid',
    value: BoxDB.Range.equal('1'),
  },
  (item) => !!item.memo,
  (item) => item.memo.includes('exp'),
).get();

// [{ uid: '1', name: 'mac', memo: 'so expensive' }]

Update multiple records by cursor

1
2
3
4
await Item.find(null, (item) => !!item.memo).update({ memo: 'new memo' });

// Before: { uid: '1', name: 'mac', memo: 'so expensive' }
//  After: { uid: '1', name: 'mac', memo: 'new memo' }

Delete multiple records by cursor

1
2
3
await Item.find(null, (item) => item.uid === '0').delete();

// Delete all of `Item.uid = '0'`

Multiple task in one transaction

If error occurs during transaction task, will be rollback to before transaction.

Transactionable methods name has $ prefix ($add, $put, $delete, $find)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
await db.transaction(
  User.$add({ _id: '3', name: 'Aiden', age: 16 }),
  Item.$add({ uid: '3', name: 'pencil', memo: 'super sharp' }),
  Item.$add({ uid: '3', name: 'eraser' }),
  Item.$add({ uid: '3', name: 'book', memo: '200p' }),
  Item.$add({ uid: '3', name: 'juice' }),
);

// Add new user: { _id: '3', name: 'Aiden', age: 16 }
// Add new item: { uid: '3', name: 'pencil', memo: 'super sharp' }
// Add new item: { uid: '3', name: 'book', memo: '200 pages' }
// Add new item: { uid: '3', name: 'eraser', memo: '' }
// Add new item: { uid: '3', name: 'juice', memo: '' }

Get records count

1
await Item.count(); // n

Clear all records

1
2
3
4
5
// A. Using cursor
await User.find().delete();

// B. Using clear()
await User.clear();

Web workers

1
2
3
4
importScripts('https://cdn.jsdelivr.net/npm/bxd@latest/dist/bxd.min.js');

// Full features available!
self.BoxDB;