© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
V. K. KotaruBuilding Offline Applications with Angularhttps://doi.org/10.1007/978-1-4842-7930-4_10

10. Dexie.js for IndexedDB

Venkata Keerti Kotaru1  
(1)
-, Hyderabad, Telangana, India
 

So far, you have seen use cases and implementations for using a database on the client side. You learned about and implemented IndexedDB. The browser API enables you to create a database, performing create/retrieve/update/delete (CRUD) operations. The functions are native to the browser. All the latest versions of the major browsers support IndexedDB. However, arguably, the IndexedDB API is complex. An everyday developer might need a simplified version.

Dexie.js is a wrapper for IndexedDB. It is a simple and easy-to-use library that is installable in your application. It is an open source repository with an Apache 2.0 license. The license allows commercial use, modifications, distribution, patent use, and private use. However, it has limitations with respect to trademark use and has no liability and warranty. Understand the agreement better while using the library for a business application you might be working on.

The chapter is an introduction to Dexie.js. It provides an overview of the library within the parameters of the Web Arcade use cases. It begins with instructions to install Dexie.js in the Web Arcade application. Next, it details how to use the library among TypeScript files. You create a new class and a service to encapsulate data access logic to IndexedDB using Dexie.js. The chapter also details how to create transactions, performing CRUD operations on the data. Toward the end, the chapter lists a few additional libraries and wrappers on top of IndexedDB.

Installing Dexie.js

Install the Dexie package.
npm i -S dexie
or
yarn add dexie
Note

The command npm i -S dexie is a short form of npm install --save dexie.

-S or --save is an option to add Dexie to the Web Arcade package. An entry will be added to package.json. This will ensure that future installs include Dexie.

Yarn does not need this option. It is implicit; it will always add the package to Web Arcade.

Web Arcade Database

Create a TypeScript class encapsulating the Web Arcade IndexedDB connection. Use this class to access the IndexedDB database web-arcade with the Dexie API. Run this command to create a TypeScript class:
ng generate class common/web-arcade-db
Use the class WebArcadeDb to specify the IndexedDB database to create and connect. You will also use this class to define object stores, indexes, etc. Add the code shown in Listing 10-1 to the new class WebArcadeDb.
01: import { Dexie } from 'dexie';
02: import { CommentsEntity } from './board-games-entity';
03:
04: const WEB_ARCADE_DB_NAME = 'web-arcade-dexie';
05: const OBJECT_STORE_GAME_COMMENTS = 'gameComments';
06: export class WebArcadeDb extends Dexie {
07:     comments: Dexie.Table<CommentsEntity>;
08:
09:     constructor() {
10:       super(WEB_ARCADE_DB_NAME);
11:       this.version(1.0).stores({
12:         gameComments: '++IdxCommentId,timeCommented,userName'
13:       });
14:       this.comments = this.table(OBJECT_STORE_GAME_COMMENTS);
15:      }
16:   }
Listing 10-1

A TypeScript Class for the Web Arcade DB

Consider the following explanation:
  • Line 6 creates a new TypeScript class, WebArcadeDb. It extends the Dexie class, which provides many out-of-the-box features including opening a database, creating stores, etc.

  • Notice that the Dexie class is imported from the dexie ES6 module (part of the Dexie library) on line 1.

  • Provide the web-arcade database name to the superclass. See line 10, the first line in the constructor. In this code sample, the TypeScript class WebArcadeDb is dedicated to one IndexedDB, web-arcade. The database name is assigned to a constant on line 4. It is used while opening a connection to the database.

Object Store/Table

Consider the following explanation that details how to use the stores() and table() APIs between lines 11 and 14:

  • The constructor also defines the object store structure. In the current example, it creates a single store called gameComments. See the string value in line 5. You may create additional object stores by including additional fields in the JSON object. It is passed in as a parameter to the stores() function.

  • The gameComments object store defines two fields, IdxCommentId and timeCommented.

  • You prefix (or postfix) ++ on the primary key. This field identifies each comment uniquely. It auto-increments for each record added to the object store.

  • The object store includes one or more fields. In this example, the object store includes two fields: timeCommented and userName. This statement creates the object store with the listed fields.

  • While inserting records into the object store, you may include many more fields. However, indexes are created only on the fields specified with the stores() API (line 12). A query is limited to the fields indexed with the object store. Hence, include any field that you may query in the future.

  • Notice that the stores function is in a version() API, which defines a version for the Web Arcade IndexedDB. As you will see in the next section, you may create additional versions of the database and upgrade.

  • Dexie uses a TypeScript class called Table to refer to an object store. See line 7 for the class variable comments. You create the variable of type Table (Dexie.Table).

  • Notice a generic type CommentsEntity passed to the Table class. The class variable comments is confined to the interface CommentsEntity. Remember, the comment entity includes all the fields related to a user comment. Revisit CommentsEntity on src/app/common/comments-entity.ts. See Listing 10-2.

  • Next, see line 14. The this.table() function returns an object store reference. The table() function is inherited from the parent classes. Notice that you provide an object store name to the table() function. It uses the name to return that particular object store, for example, a gameComments object store.

  • The returned object store is set to the class variable comments. Accessing this variable on the WebArcadeDb instance refers to the object store gameComments. For example, webArcadeDbObject.comments refers to the gameComments object store.

1: export interface CommentsEntity {
2:     IdxCommentId?: number;
3:     title: string;
4:     comments: string;
5:     timeCommented: string;
6:     gameId: number;
7:     userName:string;
8: }
Listing 10-2

Comments Entity

IndexedDB Versions

As your application evolves, anticipate changes to the database. IndexedDB supports versions to transition between the upgrades. Dexie uses the underlying API and provides a clean way to version IndexedDB.

Listing 10-3 creates the web-arcade database with one object store and three fields (one primary key and two indexes). See line 12. Imagine you need to add an additional field gameId to the index and create a new object store for board game comments.

Before you make this database change, increment the version number. Consider updating it to 1.1.

Note

In version number 1.0, the number before the decimal point is called the major version. The number after the decimal point is the minor version. As the names indicate, consider updating the major version number if there is a major change to the database structure. For a minor addition of a single field, index, or object store, update the minor version.

Next, add a new index for gameId. Include a new object store called boardGameComments with a primary key, commentId. Consider Listing 10-3. See Figure 10-1 for the result. This is an IndexedDB view using Google Chrome Developer Tools.
1: this.version(1.1).stores({
2:     gameComments: '++IdxCommentId,timeCommented, userName, gameId',
3:     boardGameComments: '++commentId'
4:   });
Listing 10-3

Upgrade Web Arcade to a New Version

Figure 10-1

New object store, index on version 11 (1.1)

Next, consider a scenario where you need to delete an object store and remove an index. Consider removing the index on the username and deleting the boardGameComments object store. Follow these instructions:
  1. 1.

    Update the version number. Consider using 1.2. This will translate to 12 on the IndexedDB.

     
  2. 2.

    Set the object store to be deleted to null. In the current example, set boardGameComments to null. See line 3 in Listing 10-4.

     
  3. 3.

    To make changes to an object store, use the upgrade() API on the database object. In the current example, we remove an index called userName on the object store gameComments and provide a callback function. The function parameter is a reference variable to the database. Consider Listing 10-4.

     
1: this.version(1.2).stores({
2:     gameComments: '++IdxCommentId,timeCommented, userName, gameId',
3:     boardGameComments: null
4:   }).upgrade( idb =>
5:     idb.table(OBJECT_STORE_GAME_COMMENTS)
6:       .toCollection()
7:       .modify( comments => {
8:         delete comments.userName;
9:       }) );
Listing 10-4

Remove Object Store and Index

  • Line 8 deletes the user on the comments object. The comments reference is obtained while modifying the object store gameComments. Remember, Dexie’s table class (and the instance) refers to an object store.

Connecting with Web-Arcade IndexedDB

Remember the thought process creating the IdbStorageAccessService. It abstracts the IndexedDB API from the rest of the application. If you choose to use Dexie instead of the native browser API, follow a similar approach and create a service. Run the following command to create a service. Provide the arbitrary name dexie-storage-access to the service.
ng g s common/dexie-storage-access
Note

The command ng g s common/dexie-storage-access is a short form of ng generate service common/dexie-storage-access.

g- generate

s- service

Similar to IdbStorageAccessService, initialize the DexieStorageAccessService at application startup. Include an init() function with the code to initialize. Use Angular’s APP_INITIALIZER and include it in the AppModule. Consider Listing 10-5. See lines 11 to 16. Notice that the app initializer invokes the init() function (line 13).
01: @NgModule({
02:     declarations: [
03:       AppComponent,
04:       /* More declarations go here */
05:     ],
06:     imports: [
07:       BrowserModule,
08:       /* additional imports go here */
09:     ],
10:     providers: [
11:       {
12:         provide: APP_INITIALIZER,
13:         useFactory: (svc: DexieStorageAccessService) => () => svc.init(),
14:         deps: [DexieStorageAccessService],
15:         multi: true
16:       }
17:       /* More providers go here */
18:     ],
19:     bootstrap: [AppComponent]
20:   })
21:   export class AppModule { }
Listing 10-5

Initialize DexieStorageAccessService at Application Startup

Initializing IndexedDB

DexieStorageAccessService initializes IndexedDB using an instance of the WebArcadeDB class (created in Listing 10-3). Use the open() function, which opens a connection to the database if it already exists. If not, it will create a new database and open the connection. Consider Listing 10-6.
01: import { Injectable } from '@angular/core';
02: import { WebArcadeDb } from 'src/app/common/web-arcade-db';
03: import { CommentsEntity } from 'src/app/common/comments-entity';
04:
05: @Injectable({
06:   providedIn: 'root'
07: })
08: export class DexieStorageAccessService {
09:   webArcadeDb = new WebArcadeDb();
10:   constructor() {}
11:   init(){
12:     this.webArcadeDb
13:     .open()
14:     .catch(err => console.log("Dexie, error opening DB"));
15:   }
16: }
Listing 10-6

Dexie Storage Access Service

Consider the following explanation:
  • Create a new class-level instance of WebArcadeDb and instantiate. It encapsulates the Web Arcade IndexedDB. See line 9 in Listing 10-9.

  • Remember that you invoked the init() function from the app module with the help of APP_INITIALIZER. Notice the definition on lines 11 to 15. This initializes by invoking open() on the IndexedDB. As mentioned earlier, it creates a database for Web Arcade if it doesn’t exist. It will open a connection to IndexedDB.

  • After initialization, the IndexedDB is open for the database operations including CRUD.

  • The open function returns a promise. If it fails, the promise is rejected. Notice line 14. This is an error handling statement when the promise is rejected. In the current example, you log a message and the error to the browser console.

Transactions

It is important to include database operations in a transaction. A transaction ensures all enclosed operations are atomic, that is, performed as a single unit. Either all the operations are performed or none is performed. This is useful to ensure the consistency of the data.

In an example, imagine you are transferring data from object store 1 to object store 2. You read and deleted data from object store 1. Imagine the user closed the browser before the update to object store 2 is complete. Without a transaction, data is lost. A transaction ensures the deletion from object store 1 is reverted if there is a failure before adding the data to object store 2. This ensures data is not lost.

Create a transaction on a WebArcadeDb object, as shown in Listing 10-7.
1: this.webArcadeDb.transaction("rw",
2:     this.webArcadeDb.comments,
3:     () => {
4:
5:     })
Listing 10-7

Create a Transaction

Consider the following explanation:
  • See line 1. A transaction is created on an instance of the WebArcadeDb object. It is a class-level variable on DexieStorageAccessService.

  • The first parameter on the transaction function is a transaction mode. Two values are possible.
    • Read: The value to the first parameter is r. The transaction can only perform read operations.

    • Read-Write: The value of the first parameter is rw. See line 1 in Listing 10-10. The transaction can perform read and write operations.

  • The second parameter is an object store reference. See line 2. The comments field points to the object store gameComments. See line 14 in Listing 10-3.

  • You may include more than one object stores in a transaction.

  • The final parameter is a function callback. It includes code to perform create, retrieve, update, or delete operations on a database.

Add

Remember , so far, that you created an object store called gameComments. Listing 10-8 adds a record to the object store.
01: addComment(title: string, userName: string, comments: string, gameId: number, timeCommented = new Date()){
02:     this.webArcadeDb
03:       .comments
04:       .add({
05:         title,
06:         userName,
07:         timeCommented: `${timeCommented.getMonth()}/${timeCommented.getDate()}/${timeCommented.getFullYear()}`,
08:         comments,
09:         gameId,
10:       })
11:       .then( id => console.log(`Comment added successfully. Comment Id is ${id}`))
12:       .catch( error => console.log(error))
13:       ;
14: }
Listing 10-8

Add a Comment Record to the Object Store

Consider the following explanation:
  • See line 2. You use an instance of the WebArcadeDb object. It is a class-level variable on DexieStorageAccessService.

  • The add() function inserts a record into the object store (line 4). The record includes various comment fields including the title, username, comment date and time, comment description, and ID of the game on which the comment is added.

  • The add() function returns a promise. If the add action is successful, the promise is resolved. See line 11, which logs the comment ID (primary key) to the browser console. If the add action fails, the promise is rejected. The catch() function on line 12 runs, which prints the error information on the browser console.

Delete

Perform the delete action by using the database object, webArcadeDb. Call the delete() API on the database. It needs a comment ID, the primary key as an input parameter. Consider Listing 10-9.
1: deleteComment(id: number){
2:     return this.webArcadeDb
3:       .comments
4:       .delete(id)
5:       .then( id => console.log(`Comment deleted successfully.`))
6:       .catch(err => console.error("Error deleting", err));
7: }
8:
Listing 10-9

Delete a Comment Record in the Object Store

Consider the following explanation:
  • See line 2. You use an instance of the WebArcadeDb object. It is a class-level variable on DexieStorageAccessService.

  • The delete() function deletes a record from the object store (line 4). The record to be deleted is identified by the comment ID, a primary key.

  • The delete() function returns a promise. If the delete action is successful, the promise is resolved. See line 5, which logs a success message to the browser console. If the delete action fails, the promise is rejected. The catch() function on line 6 runs, which prints the error information on the browser console.

Update

Perform the update action by using the database object, webArcadeDb. Call the update() API on the database. It needs a comment ID, which is the primary key, as the first input parameter. It selects the record to be updated using the comment ID. It uses an object with a key path and a new value to be updated. Consider Listing 10-10.
1: updateComment(commentId: number, newTitle: string, newComments: string){
2:     this.webArcadeDb
3:       .comments
4:       .update(commentId, {title: newTitle, comments: newComments})
5:       .then( result => console.log(`Comment updated successfully. Updated record ID is ${result}`))
6:       .catch(error => console.error("Error updating", error));
7:   }
Listing 10-10

Update a Comment Record

Consider the following explanation:
  • See line 2. You use an instance of the WebArcadeDb object. It is a class-level variable on DexieStorageAccessService.

  • The update() function updates a record on the object store (line 4). The record to be updated is identified by the comment ID, a primary key. The second parameter is an object with key-value pairs of the values to be updated. Notice that a key identifies the field to be updated on the record, in the object store.

  • For example, the following code snippet updates a record with the comment ID 1, a primary key. The next two parameters are the new title and description, respectively.

    this.updateComment(1, "New title", "new comment description");
Figure 10-2 shows the result.
Figure 10-2

Update result

  • The update() function returns a promise. If the update action is successful, the promise is resolved. See line 5, which logs a success message to the browser console. If the update action fails, the promise is rejected. The catch() function on line 6 runs, which prints the error information on the browser console.

Note

The update() function updates specific fields on a record in an object store. To replace the entire object, use put().

Retrieve

Dexie provides a comprehensive list of functions to query and retrieve data from IndexedDB. Consider the following:
  • get(id): Selects a record in the object store using an ID/primary key. The ID is passed in as a parameter. The get() function returns a promise. On successful get, the then() callback function returns results.

  • bulkGet([id1, id2, id3]): Selects multiple records in the object store. IDs are passed in as a parameter. The bulkGet() function returns a promise. On a successful get, the then() callback function returns results.

  • where({keyPath1:value, keyPath2: value…, keyPath: value}): Filters records by fields specified with keyPath and the given value.

  • each(functionCallback): Iterates through the objects in an object store. The API invokes the provided function callback asynchronously. Consider Listing 10-11.

1: getAllCachedComments(){
2:     this.webArcadeDb
3:       .comments
4:       .each( (entity: CommentsEntity) => {
5:         console.log(entity);
6:       })
7:       .catch(error => console.error("Error updating", error));
8: }
9:
Listing 10-11

Get All Cached Comments from the gameComments Object Store

Consider the following explanation:
  • Line 2 uses an instance of the WebArcadeDb object. It is a class-level variable on DexieStorageAccessService.

  • Line 4 iterates through each record in the gameComments object store.

  • The callback function uses the parameter of type CommentsEntity. As and when the callback is invoked asynchronously, data confining to the CommentsEntity interface is expected to be returned.

  • Line 5 prints the entity to the browser console .

More Options

In this book, you have seen the IndexedDB API supported natively by the browser. This chapter provided an introduction to Dexie.js, a wrapper intended to simplify access to the database.

The following are a few additional options to consider. While the implementation details are out of the scope of this book, consider reading and learning further about these libraries. All these libraries use IndexedDB underneath.
  • Local Forage: This provides simple API and functions. The API is similar to local storage. On the legacy browsers that do not support IndexedDB, Local Forage provides a polyfill. It has the ability to fall back to WebSQL or local storage. It is an open source library with an Apache 2.0 license.

  • ZangoDB: This provides a simple and easy-to-use API that mimics MongoDB. The library uses IndexedDB. The wrapper profiles an easy API for filtering, sorting, aggregation, etc. It is an open source library with an MIT license.

  • JS Store: This provides Structured Query Language (SQL) like API for IndexedDB. It provides all the features IndexedDB provides in an easy-to-understand API similar to traditional SQL. It is an open source library with an MIT license.

  • PouchDB: This provides an API to synchronize client-side, offline data with CouchDB. It’s a highly useful library for applications using a CouchDB server-side back end. It is an open source library with an Apache 2.0 license.

Summary

This chapter introduced Dexie.js. It provided a basic understanding of the library within the parameters of the Web Arcade use cases. It listed instructions to install the Dexie.js NPM package to the web arcade application.

Also, the chapter listed a few additional libraries and wrappers on top of IndexedDB. While the implementation details are out of scope of this book, it lists the names and one-liner introduction for further learning.

Exercise
  • Update the game details component to use the Dexie storage access service for caching comments while the application is offline.

  • Update the online event to use the Dexie storage access service to retrieve records when the application returns online. Integrate with the server-side service to synchronize the data and delete local records using the Dexie.js API.

  • Provide the ability to update a comment using the Dexie storage access service.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.209.184