Small and simple model-based IndexedDB wrapper.
If you have ever dealt with IndexedDB, you've probably noticed that its API is not really usable in the modern world of promises and async
functions.
This library is designed the way that you wouldn't need to touch the weird native API.
If you only need a promise wrapper for the native API or a simple key-val store, you can take a look at idb or idb-keyval.
If you need something more advanced idb-model
is what you need.
The library consists of two classes: Database
and Model
.
You migrate the database and perform transactions through an instance of Database
and you manage records in an object store through your custom subclasses of Model
(if you're familiar with sequelize this should be easy to understand as idb-model
is inspired by it).
This class is responsible for connecting to the database and opening transactions to it.
- name (required) - database name.
- options (optional):
- options.transactionMode (optional) - is either
"readonly"
or"readwrite"
- default level for Database#transaction. Default is"readonly"
. - options.onBlocked (optional) - is just passed to this.
- options.onVersionChange (optional) - is just passed to this.
- options.transactionMode (optional) - is either
Closes the connection.
db.close();
Deletes the database. Returns Promise
.
(async () => {
await db.delete();
})();
Returns Promise
resolved with the instance of IDBDatabase.
Used primarily internally, and you probably wouldn't need this in most cases, but if you need to access the native API use this method.
(async () => {
const connection = await db.getConnection();
console.assert(connection instanceof IDBDatabase);
})();
- migrations (required) - An array of functions (may be async) that take two arguments:
- db - an IDBDatabase instance.
- transaction - a version change transaction. You can pass it to any
Model
method that accepts transaction.
Returns Promise
.
Use this function to migrate between different versions of your database.
Note: if a migration is an async
function you can't really fetch resources or perform any other non-idb-related async actions because IndexedDB transactions auto-close when there is nothing to do.
But you can await
some Model
methods, but only if you're passing the transaction
parameter to them.
Also if you're going to use some native transaction API in a migration, use only the transaction
from the parameter.
For example:
const db = new Database('db');
class User extends Model {
static modelName = 'users';
static primaryKey = 'id';
}
db.model(User);
(async () => {
await db.migrate([
(db) => {
db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
},
async (db, transaction) => {
// splits fullName into firstName and lastName
await User.update((user) => {
[user.firstName, user.lastName] = user.fullName.split(' ');
delete user.fullName;
}, { transaction });
}
]);
})();
- model (required) - a subclass of
Model
.
Attaches model
to the database instance.
class User extends Model {
static modelName = 'users';
static primaryKey = 'id';
}
db.model(User);
- storeNames (required) - a string or string array of store names that this transaction is using.
- mode (optional) -
"readonly"
or"readwrite"
. If not specified thendb.transactionMode
is used that was specified when creating the database. - callback (required) - function that takes transaction argument that can then be passed to any
Model
method.
Returns Promise
resolved with the return value of callback when the transaction is completed.
Performs a transaction to the database.
Note: Don't await
asynchronous actions inside the callback before doing something with transaction
as the transaction completes when there's nothing to do. Example:
const db = new Database('db', {
transactionMode: 'readwrite'
});
class User extends Model {
static modelName = 'users';
static primaryKey = 'id';
}
async function pay(payer, receiver, amount) {
const result = await db.transaction('users', async (transaction) => {
const payerFromDb = await User.findByPrimary(payer.id, { transaction });
if (payerFromDb.balance < amount) {
return false;
}
const receiverFromDb = await User.findByPrimary(receiver.id, { transaction });
receiverFromDb.balance += amount;
payerFromDb.balance -= amount;
await User.bulkSave([payerFromDb, receiverFromDb], { transaction });
return true;
});
console.log(result);
}
To manage records in an object store use this class, but not directly - only through your custom subclasses. Example:
class User extends Model {
static modelName = 'users';
static primaryKey = 'id';
}
const user = new User({
name: 'John',
age: 30
});
(async () => {
await user.save();
})();
- values (required) - object with values of the instance.
Model.defaultValues
is assigned to the instance before them.
- modelName (required): used to select
objectStore
from the database. - primaryKey (required): for now
save
anddelete
operations are primary-key-based, so that all of your models have to have a primary key. - fields (optional): an array of instance fields to save. By default all instance enumerable fields are saved. You can use this field to filter out the fields that don't need to be stored. To customize stored fields more use Model#toJSON hook.
- defaultValues (optional): an object with default values.
- values (required) - object with values of the instance.
Returns new instance (array of instances for bulkBuild
).
Alias of new Model(values)
.
const user = User.build({
name: 'John',
age: 30
});
const users = User.bulkBuild([
{ name: 'John', age: 30 },
{ name: 'Jack', age: 25 }
]);
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
Clears the object store. Returns Promise
.
(async () => {
await User.clear();
})();
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
Counts the records in the object store. Returns Promise
resolved with the number of records.
(async () => {
console.log('number of users', await User.count());
})();
- values (required) - object with values of the instance.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeSave hook.
Creates the instance(s) using values
and saves it (them) in the object store right away.
Assigns the new primary key value to the instance after save.
Returns Promise
resolved with the instance(s). (In bulkCreate
all records are created in one transaction).
(async () => {
const user = await User.create({
name: 'John',
age: 30
});
// in case primary key is 'id'
console.log('user id', user.id);
console.log(user);
const users = await User.bulkCreate([
{ name: 'John', age: 30 },
{ name: 'Jack', age: 25 }
]);
console.log(users);
})();
- filter (optional) - callback that takes an instance. If returns truthy value then the record is deleted, otherwise not.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeDelete hook.
Deletes records that match the filter. If no filter specified, all records are deleted. Returns Promise
resolved with the deleted instances.
(async () => {
// deletes all records
await User.delete();
// deletes all records that have age < 20
await User.delete(({ age }) => age < 20);
})();
- instances (required) - an array of instances of the model to delete.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeDelete hook.
Returns Promise
resolved with the instances.
Deletes instances from the object store.
Under the hood just calls delete method for each instance.
The main difference is that all records are deleted in one transaction.
(async () => {
const users = await User.bulkCreate([
{ name: 'John', age: 30 },
{ name: 'Jack', age: 25 }
]);
await User.bulkDelete(users);
})();
- filter (optional) - callback that takes an instance. If returns truthy value then the record is included, otherwise not.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
(async () => {
const users = await User.findAll(({ age }) => age < 20);
console.log(users);
})();
Returns Promise
resolved with an array of instances that match the filter. If no filter specified, all records are included.
- filter (optional) - callback that takes an instance. If returns truthy value then this record is returned.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
(async () => {
const user = await User.findOne(({ name }) => name === 'John');
console.log(user);
})();
Returns Promise
resolved with the first instance that matches the filter or null
if no records match the filter.
- primary (required) - value of the primary key field of the record to find.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
(async () => {
const user = await User.findByPrimary(1);
console.log(user);
})();
Returns Promise
resolved with the instance that matches primary
or null if no records match.
- instances (required) - an array of instances of the model to save.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
(async () => {
const users = await User.bulkCreate([
{ name: 'John', age: 30 },
{ name: 'Jack', age: 25 }
]);
users.forEach((user) => user.age += 1);
await User.bulkSave(users);
})();
Saves multiple instances in one transaction. Returns Promise
resolved with the instances.
- values (required) - either an object with fields to update or a callback that is called with an instance to update. In case of callback don't return a new value, rather modify the instance.
- filter (optional) - callback that takes an instance. If returns truthy value then the record is updated, otherwise not.
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeSave hook.
Updates records that match the filter. If no filter specified, all records are updated. Returns Promise
resolved with the updated instances.
(async () => {
// increments age by 1 in all records
await User.update((user) => user.age += 1);
// increments age by 1 in all records that have age < 20
await User.update(
(user) => user.age += 1,
({ age }) => age < 20
);
// sets age to 1 in all records that have age < 20
await User.update(
{ age: 1 },
({ age }) => age < 20
);
})();
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeDelete hook.
Deletes the record using the primary key.
If the instance doesn't have the primary key field, then the method does nothing.
Returns Promise
resolved with the instance.
(async () => {
const user = await User.findByPrimary(1);
await user.delete();
})();
- options (optional):
- options.transaction (optional) - if present, the operation is performed in this transaction.
- options.storeNames (optional) - an array of object stores, that may be needed for beforeSave hook.
Saves the record using the primary key.
The record is updated if the instance has the primary key field, otherwise the record is added.
Returns Promise
resolved with the instance.
(async () => {
// update
const user = await User.findByPrimary(1);
user.age += 1;
await user.save();
// create
const user = new User({
name: 'John',
age: 30
});
await user.save();
})();
- transaction - the transaction which deletes the record.
- options - the options with which a delete method was called.
Set this method in your model to add some operations before the record is deleted using Model.delete, Model.bulkDelete or Model#delete.
The method may be asynchronous, though you should perform only asynchronous actions related to the transaction
.
options.storeNames
from delete methods is used to open a delete transaction, so that if you need to do something involving other stores in the hook, specify options.storeNames
in the corresponding delete method.
- transaction - the transaction which deletes the record.
- options - the options with which a save (or update) method was called.
Set this method in your model to add some operations before the record is saved using Model.create, Model.bulkCreate, Model.bulkSave, Model.update or Model#save.
The method may be asynchronous, though you should perform only asynchronous actions related to the transaction
.
options.storeNames
from save (or update) methods is used to open a save transaction, so that if you need to do something involving other stores in the hook, specify options.storeNames
in the corresponding save (or update) method.
Set this method in your model to customize what is saved to the database returning the desired object.
If this method is specified Model.fields
is not used.
Here is an example of a user model:
// you need to create a separate interface to pass it to Model
interface UserAttributes {
id: number;
name: string;
age: number;
job: string;
}
// you need User to extend UserAttributes, so that you can access your custom fields
interface User extends UserAttributes {}
// first argument is values interface
// second argument is optional fields that are not necessary to set when creating an instance
class User extends Model<UserAttributes, 'id' | 'job'> {
static modelName = 'users';
// this is a hack so that ts recognizes 'id' as a key of User
static primaryKey = 'id' as 'id';
// you need to set default values for optional parameters here, except the primaryKey field - this and required fields are optional here
static defaultValues = {
job: ''
};
}
-
Also the library exports a small helper -
promisifyRequest
, that takes an IDBRequest instance and "promisifies" it:promisifyRequest(request[, defaultValue])
Returns a
Promise
resolved with the request result ordefaultValue
. Use this helper if you need some native API requests to be promisified. -
For now
idb-model
doesn't support IDBKeyRange API, IDBIndex API and IDBCursor API. Although the first one and the second one may be useful together, the third one does not seem very useful considering that it's already used internally in someModel
methods.