I'm a developer & content creator, based in Ghent

Using UUID primary keys in Dexie

For my point-of-sales application I use Dexie as a wrapper for IndexedDB - and it works great.

I experimented with Dexie.Syncable to sync changes between multiple devices, but we ended up using our own backend with a REST API as the source of truth. Dexie.Syncable has Dexie.Observable as a dependency, and this specific dependency adds - as the name suggests - observability to your database. Unfortunately, this comes at a pretty big performance impact, as every CRUD operation comes at the expense of being registered in the _changes table as well.

Dexie.Observable adds a few meta tables your database to maintain change tracking - and to be able to track changes consistently across devices / tabs - you cannot rely on auto-incrementing indices. That's why Observable adds the capability to define an auto-generated UUID as a primary key in your table by using the $$ prefix as follows:

db.version(1).stores({
    friends: "$$uuid, name"
});

This automatic UUID generation feature is pretty handy, so I decided to extract it into its own plugin: Dexie.UUIDPrimaryKey.js

/**
 * This class allows you to use UUID primary keys in Dexie by defining them in the store using two dollar signs ($$), eg:
 *
 * db.version(1).stores({
 *   orders: '$$id, price',
 *   order_products: '$$id, product, order_id'
 * });
 *
 * Parts are adapted from dexie-observable.js. 
 */

import Dexie from 'dexie';
import { v4 as uuidv4 } from 'uuid';

/**
 * DexieUUIDPrimaryKey plugin
 * @param db
 * @constructor
 */
function DexieUUIDPrimaryKey(db) {
    // Override the _parseStoresSpec method with our own implementation
    db.Version.prototype._parseStoresSpec = Dexie.override(db.Version.prototype._parseStoresSpec, overrideParseStoresSpec);
    // Override the open method with our own implementation
    db.open = Dexie.override(db.open, prepareOverrideOpen(db));
}

/**
 * This function overrides the parseStoresSpec method of Dexie to allow for UUID primary keys.
 * @param origFunc
 * @returns {(function(*, *): void)|*}
 */
function overrideParseStoresSpec(origFunc) {
    return function(stores, dbSchema) {
        origFunc.call(this, stores, dbSchema);
        Object.keys(dbSchema).forEach(function(tableName) {
            let schema = dbSchema[tableName];
            if (schema.primKey.name.indexOf('$$') === 0) {
                schema.primKey.uuid = true;
                schema.primKey.name = schema.primKey.name.substr(2);
                schema.primKey.keyPath = schema.primKey.keyPath.substr(2);
            }
        });
    };
}

/**
 * This function prepares the hook that will trigger on creation of a new record
 * @param table
 * @returns {function(*, *): undefined}
 */
function initCreatingHook(table) {
    return function creatingHook(primKey, obj) {
        let rv = undefined;
        if (primKey === undefined && table.schema.primKey.uuid) {
            primKey = rv = uuidv4();
            if (table.schema.primKey.keyPath) {
                Dexie.setByKeyPath(obj, table.schema.primKey.keyPath, primKey);
            }
        }

        return rv;
    };
}

/**
 * This function prepares the hook that will trigger on opening the database and will loop through all tables to add the creating hook.
 * @param db
 * @returns {function(*): function(): *}
 */
function prepareOverrideOpen(db) {
    return function overrideOpen(origOpen) {
        return function () {
            Object.keys(db._allTables).forEach(tableName => {
                let table = db._allTables[tableName];
                table.hook('creating').subscribe(initCreatingHook(table));
            });
            return origOpen.apply(this, arguments);
        }
    }
}

// Register addon:
Dexie.UUIDPrimaryKey = DexieUUIDPrimaryKey;
Dexie.addons.push(DexieUUIDPrimaryKey);

export default Dexie.UUIDPrimaryKey;

By importing this class, you're able to add UUID primary keys using the $$ prefix, without using the entire Observable class - so you can keep your CRUD operations smooth & performant!

Subscribe to my monthly newsletter

No spam, no sharing to third party. Only you and me.