Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Queries and Indexes

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Queries

We can retrieve objects from our table with the get and where methods.

get retrieves an object by its primary key.

where does an advanced query.

For example, we can use get by writing:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: '++id,name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    await db.friends.put({
      name: "james",
      age: 22
    })
    const friend = await db.friends.get(1)
    console.log(friend)
  } catch (error) {
    console.log(error);
  }
})()

We call get with the id value of the entry to get.

Then that returns a promise with the result we want to get.

To make more complex queries, we can use the where query.

For instance, we can write:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: '++id,name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    await db.friends.put({
      name: "james",
      age: 22
    })
    const friendCount = await db.friends.where('age').above(25).count()
    console.log(friendCount)
  } catch (error) {
    console.log(error);
  }
})()

We add 2 entries to the friends table.

Then we call where with the column to search for.

above searches for anything with the value above a given value.

count returns the count of the results.

We can call more methods to make more advanced queries:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: '++id,name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28,
      isCloseFriend: true
    })
    await db.friends.put({
      name: "james",
      age: 22
    })
    const friendCount = await db.friends
      .where('age')
      .between(37, 40)
      .or('name')
      .anyOf(['mary', 'james'])
      .and((friend) => {
        return friend.isCloseFriend;
      })
      .limit(10)
      .each((friend) => {
        console.log(friend);
      });
  } catch (error) {
    console.log(error);
  }
})()

or lets us combine one or more conditions with an OR operator.

anyOf searches for any of the values in the array.

and combines one or more conditions with an AND operator.

limit limits the number of items returned.

each lets us iterate through each result.

Detailed Schema Syntax

We can define a schema with Dexie’s schema syntax.

Parts of the syntax include:

  • ++keyPath — autoincrement primary key.
  • ++ — hidden autoincrement primary key.
  • keyPath — non-autoincrement primary key.
  • (blank) — hidden primary key.
  • keyPath — the keyPath is indexed
  • &keyPath — keyPath is indexed and the keys must be unique.
  • *keyPath — the key is an array and eah array value is regarded as a key to the object
  • [keyPath1+keyPath2] — compound index for keyPath1 and keyPath2 .

We can use them to add indexes by writing:

(async () => {
  const db = new Dexie('MyDatabase');
  db.version(1).stores({
    friends: '++id,name,age',
    pets: 'id, name, kind',
    cars: '++, name',
    enemies: ',name,*weaknesses',
    users: 'meta.ssn, addr.city',
    people: '[name+id], &id'
  });
})()

Conclusion

We can make queries and create indexes easily in our IndexedDB database with Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Indexes, Seed Data, and Promises

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Indexes

We can add and drop indexes as we wish.

For example, we write:

const db = new Dexie("dbs");
db.version(1).stores({
  foo1: 'id,x,y,z',
  foo2: 'id,x,y,z',
  foo3: 'id,x,y,z'
});
db.version(2).stores({
  foo1: 'id,x,z'
});
db.version(3).stores({
  foo2: 'id, x, x2, y, z'
});
db.version(4).stores({
  foo3: null
});

We created the foo1 , foo2 , and foo3 stores with indexes x , y and z .

Then to delete index y from foo1 , we write:

db.version(2).stores({
  foo1: 'id,x,z'
});

To add index x2 to foo2 , we write:

db.version(3).stores({
  foo2: 'id, x, x2, y, z'
});

And to drop table foo3 , we write:

db.version(4).stores({
  foo3: null
});

The populate Event

If we need initial data to be added to our database, we can watch for the populate event and add data initial data in the callback for the event.

For example, we write:

(async () => {
  const db = new Dexie("orders_database");
  await db.version(1).stores({
    orders: "++id,headline,description,statusId",
    statuses: "++id,name,openess"
  });

  db.on("populate", async () => {
    await db.statuses.add({
      id: 1,
      name: "opened",
      openess: true
    });
    await db.statuses.add({
      id: 2,
      name: "cancelled",
      openess: false
    });
    await db.statuses.add({
      id: 3,
      name: "shipped",
      openess: false
    });
    await db.statuses.add({
      id: 4,
      name: "delivered",
      openess: false
    });
  });
})()

to listen to the populate event and add some data when it’s emitted.

Promises

Dexie comes with a promise-based API.

For example, we can write:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    const friend = await db.friends.get('mary');
    const arr = await db.friends.where('name').startsWithIgnoreCase('mary').toArray();
    console.log(arr)
  } catch (error) {
    console.log(error);
  }
})()

We create the friends_database database with the friends table.

Then we add some data to the friends store with the db.friends.put method.

Then we get the data with the db.friends.where method with the column name we’re searching.

startsIgnoreCase lets us search by column by the text it starts with.

toArray returns a promise that resolves to an array of results.

Then we log the data with console.log .

All Dexie async methods return a promise.

For example, if we have:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    const friend = await db.friends.get('mary');
    const arr = await db.friends.toArray();
    console.log(arr)
  } catch (error) {
    console.log(error);
  }
})()

That’s the same as:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    const friend = await db.friends.get('mary');
    db.friends.toArray(result => console.log(result));
  } catch (error) {
    console.log(error);
  }
})()

But the promise is more convenient.

Conclusion

We can add index, add initial data, and use the promise API provided by Dexie.

Categories
JavaScript APIs

IndexedDB Manipulation with Dexie — Transactions and Versioning

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Transactions

When we do more than one operation to our database in sequence, we would normally use a transaction.

Transactions are only completed when the operation succeeds.

This is to ensure that we catch errors and don’t have any partially completed operations done to ur database.

Any errors will cause the operation to roll back.

We can use it to all the write operations synchronously without the need to wait for it to finish before starting the next one.

For example, we can write:

(async () => {
  const db = new Dexie("FriendsAndPetsDB");
  await db.version(1).stores({
    friends: "++id,name,isCloseFriend",
    pets: "++id,name,kind"
  });
  await db.open();
  db.transaction("rw", db.friends, db.pets, async () => {
    await db.friends.add({
      name: "james",
      isCloseFriend: true
    });
    await db.pets.add({
      name: "mary",
      kind: "dog",
      fur: "long"
    });
  })
})()

to add entries to our friends and pets store simultaneously.

We call db.transaction to start a transaction.

The first argument is the permissions we grant for the transaction.

r is for read, and w is for write.

db.friends and db.pets are the data stores.

The callback has the code that we want to run in the transaction.

Database Versioning

Database versioning is essential when working with IndexedDB.

For example, we can write:

(async () => {
  const db = new Dexie("FriendsDB");
  db.version(1).stores({
    friends: "++id,name"
  });
  db.friends.put({
    name: "james",
    phone: "123456",
    email: "james@edxample.com",
    age: 20
  });
})()

to create version 1 of our FriendsDB with:

const db = new Dexie("FriendsDB");
db.version(1).stores({
  friends: "++id,name"
});

We have the id primary key column, which autoincrements as indicated by the ++ operator.

id is an index on the property name.

We may store other properties as we wish.

If we want to add another column to the index, then we increment the version number by writing:

await db.version(2).stores({friends: "++id,name,age"});

We add the age column to the index and increment the database version to 2.

If we need to change the data architecture, then we need to increment the database version and call the upgrade method.

For example, we write:

(async () => {
  const db = new Dexie("FriendsDB");
  db.version(1).stores({
    friends: "++id,name"
  });
  db.version(2).stores({
    friends: "++id,name,age"
  });
  db.version(3).stores({
    friends: "++id,age,firstName,lastName"
  }).upgrade(tx => {
    return tx.table("friends").toCollection().modify(friend => {
      const [firstName, lastName] = friend.name.split(' ');
      friend.firstName = firstName;
      friend.lastName = lastName;
      delete friend.name;
    });
  });
})()

to upgrade the database version with the version method.

Then to update the database structure, we call the modify method with a callback to update existing data to the new architecture.

Now all the existing data would upgrade for existing users.

Conclusion

Dexie supports transactions so that we only commit complete operations.

Also, we can update the indexes and schema by upgrading the database version.

Categories
JavaScript APIs

Getting Started with IndexedDB Manipulation with Dexie

IndexedDB is a way to store data in the browser.

It lets us store larger amounts of data than local storage in an asynchronous way.

Dexie makes working with IndexedDB easier.

In this article, we’ll take a look at how to start working with IndexedDB with Dexie.

Getting started

We can get started by adding Dexie with a script tag:

<script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>

Then we can use it to create our data.

Then we can use it create a database, and write and read data from it.

To do this, we write:

const db = new Dexie("friend_database");
(async () => {
  try {
    await db.version(1).stores({
      friends: 'name,age'
    });
    await db.friends.put({
      name: "mary",
      age: 28
    })
    const friend = await db.friends.get('mary');
    console.log(friend.age);
  } catch (error) {
    console.log(error);
  }
})()

We create the database with the Dexie constructor.

Then we create a store in the database with:

await db.version(1).stores({
  friends: 'name,age'
});

We add the friends store into the database with the name and age fields indexed.

Indexed columns can be used to search for an entry.

Then we add the data with the put method:

await db.friends.put({
  name: "mary",
  age: 28
})

Then we get an entry with the given indexed column with:

const friend = await db.friends.get('mary');

Then get the value of the age field with:

console.log(friend.age);

Using Dexie as a Module

We can use Dexie withn the dexie module.

To install it, we run:

npm i dexie

Then can write the same code with:

import Dexie from "dexie";
const db = new Dexie("friend_database");

(async () => {
  try {
    await db.version(1).stores({
      friends: "name,age"
    });
    await db.friends.put({
      name: "mary",
      age: 28
    });
    const friend = await db.friends.get("mary");
    console.log(friend.age);
  } catch (error) {
    console.log(error);
  }
})();

The Dexie Class

The Dexie class is both a class and a namespace.

The Dexie instance represents a database connection.

It can also be used as an export area for functions, utilities, and classes.

If it’s used in the browser as a script tag, then only the window.Dexie property is added.

If it’s used as a module, then it’s available as a default export.

The Table Class

The table represents an object-store.

We have direct access to instances of Table for each object store that we’ve denied in our schema.

For example, if we have:

(async () => {
  const db = new Dexie("FriendsAndPetsDB");
  await db.version(1).stores({
    friends: "++id,name,isCloseFriend",
    pets: "++id,name,kind"
  });
  await db.open();
  await db.friends.add({
    name: "james",
    isCloseFriend: true
  });
  await db.pets.add({
    name: "mary",
    kind: "dog",
    fur: "long"
  });
})()

Then we created the friends and pets store.

db.friends and db.pets are the table instances.

And we can manipulate data with them.

db.open opens the database connection.

We called db.friends.add to add data to the db.friends store.

And we called db.pets.add to add data to the db.pets store.

Conclusion

We can manipulate IndexDB data easily with the Dexie library.

Categories
Deno

Deno — OS Signals, File System Events, and Module Metadata

Deno is a new server-side runtime environment for running JavaScript and TypeScript apps.

In this article, we’ll take a look at how to get started with developing apps for Deno.

Handle OS Signals

We can handle signals with the Deno.signal method.

For example, we can write:

index.ts

console.log("Press Ctrl-C");
for await (const _ of Deno.signal(Deno.Signal.SIGINT)) {
  console.log("interrupted!");
  Deno.exit();
}

If we press Ctrl+C, we’ll trigger the sigint signal to interrupt the program.

We watch for the signal with the for-await-of loop.

Deno.Signal.SIGINT is the object for the sigint signal.

We call Deno.exit to exit the program.

We run the program by running:

deno run --unstable index.ts

Also, we can write it as a promise.

For instance, we can write:

index.ts

console.log("Press Ctrl-C to end the program");
await Deno.signal(Deno.Signal.SIGINT);
console.log("interrupted!");
Deno.exit();

to watch for the sigint signal.

Stop Watching Signals

We can stop watching signals by calling the sig.dispose method.

For example, we can write:

index.ts

const sig = Deno.signal(Deno.Signal.SIGINT);
setTimeout(() => {
  sig.dispose();
  console.log("No longer watching SIGINT signal");
}, 5000);

console.log("Watching SIGINT signals");
for await (const _ of sig) {
  console.log("interrupted");
}

In the setTimeout callback. we call the sig.dispose method to stop watching the sigint signal.

The for-await-of loop exits after 5 seconds when sig.dispose is called.

File System Events

We can watch for file system events with the Deno.watchFs method.

For example, we can write:

index.ts

const watcher = Deno.watchFs(".");
for await (const event of watcher) {
  console.log(event);
}

We watch the folder the script is in with Deno.watchFs .

We get the event from the event object.

Then we something like:

index.ts

{ kind: "create", paths: [ "/home/runner/IntelligentWorthwhileMice/./lock.json" ] }
{ kind: "modify", paths: [ "/home/runner/IntelligentWorthwhileMice/./lock.json" ] }
{ kind: "access", paths: [ "/home/runner/IntelligentWorthwhileMice/./lock.json" ] }
{
  kind: "create",
  paths: [ "/home/runner/IntelligentWorthwhileMice/./.3s18gah600nwj.foo.txt~" ]
}
{
  kind: "access",
  paths: [ "/home/runner/IntelligentWorthwhileMice/./.3s18gah600nwj.foo.txt~" ]
}
{
  kind: "modify",
  paths: [ "/home/runner/IntelligentWorthwhileMice/./.3s18gah600nwj.foo.txt~" ]
}
{ kind: "modify", paths: [ "/home/runner/IntelligentWorthwhileMice/./foo.txt" ] }
{
  kind: "modify",
  paths: [
    "/home/runner/IntelligentWorthwhileMice/./.3s18gah600nwj.foo.txt~",
    "/home/runner/IntelligentWorthwhileMice/./foo.txt"
  ]
}

displayed.

Then we can run it with:

deno run --allow-read index.ts

to watch for any file system events in the folder.

Module Metadata

We can get module meta with the import.meta property.

For example, we can write:

console.log(import.meta.url);
console.log(Deno.mainModule);
console.log(import.meta.main);

Then we see something like:

file:///home/runner/IntelligentWorthwhileMice/index.ts
file:///home/runner/IntelligentWorthwhileMice/index.ts
true

from the console output when we run:

deno run --allow-read --unstable index.ts

import.meta.url and Deno.mainModule both get the path of the module.

And import.meta.main returns true if it’s the entry point module.

Conclusion

We can handle OS signals, get module metadata, and watch file system events with Deno.