A CRUD JavaScript Class

By  on  

A couple of weeks ago, I started making a little to-do list app' in JavaScript, with history, localStorage and quite a couple of things in order to get better at JavaScript. After a couple of hours, I decided to externalize the storage in an other class to keep things clean.

Once I was done with this little project, I thought I could make the storage class a bit more generic to be used pretty much everywhere we need to store values as key/value pairs. Thus the article, explaining how I did it.

If you're only interested in the code, have a look at the GitHub repo or this pen.

What Does it Do?

The main idea was to provide a simple and tiny CRUD API (<2Kb once gzipped). Basically, you instantiate a database, inserts objects (documents if you're a fan of MongoDB) and store them wherever you want. Then you can retrieve, update and delete documents. Pretty easy.

At first I hard-coded the usage of localStorage, then with the help of Fabrice Weinberg and Valérian Galliat, I managed to externalize the driver (what does the storage) so that you can plug the CRUD class to whatever suits your needs. Thanks a lot mates!

What is the Driver?

The driver is what actually stores your data. It is the interface that deals with persistence. To put it simple, the Database class manipulate your data while the driver stores them.

You can code your own driver or use the one I made which relies on DOM Storage (either localStorage or sessionStorage, depending on how your initialize it). The driver can rely on any storage system that supports key/value pairs (DOM Storage, Redis...). Also, it has to implement the 3 methods: getItem, setItem and removeItem.

How Does the "Quick Search" Work?

The thing is I wanted to be able to retrieve documents not only by ID, but also by searching for a couple of criterias (basically an object with properties/values). To do this, there are not thousand of solutions: you have to loop through all the documents stored in the database, then for each one iterate over all its properties and check whether they match the ones from the object given to the find function.

While this process does work, it can become painfully slow when you have hundreds of documents and are looking for a match between several properties. I needed something faster. Here comes what I call "quicksearch".

The main idea is to index the properties which are most likely to be used when searching for documents. Let's say you store users, like this one:

var dev = {
  name: 'Hugo',
  age: 22,
  job: 'dev'
}

When instanciating the database, you could pass the Database constructor indexedKeys: ['job', 'name'] to inform the class that on every operation, it has to index those properties in order to perform quick searches on those. Here is what happen when you insert the dev into the database:

  1. It adds a unique key (default is id) to the object in order to be able to identify it later
  2. It tells the driver to store the object
  3. The driver serializes and stores the object like this "{namespace}:{id}": "{serialized object}" (where {namespace} is the name of the database and {id} is the unique id assigned in step 1)
  4. It loops through all properties of the object to check if some of them have to be indexed. For each of them, it stores an entry like this "{namespace}:{property}:{value}": "{array of IDs}" so:
    • "MyDatabase:name:Hugo": "[1]"
    • "MyDatabase:job:dev": "[1]"

Now whenever you want to look for all documents which have Hugo as a name, the find function can perform a quick search by directly looking into the "MyDatabase:name:Hugo" entry to retrieve the unique ID of all of them. Fast and efficient.

How Do You Use It?

Instantiating a database

As seen before, the indexedKeys property aims at speeding up the search. By setting some keys to be indexed, searching for those keys will be way faster. In any case, you can search for any key, even those which are not indexed.

var db = new Database({
  name: 'MyDatabase',
  indexedKeys: ['job', 'age']
})

Inserting a new document

var obj = {
  name: 'Hugo',
  age: 22,
  job: 'dev'
}

var id = db.insert(obj)

Updating a document

If you want to update a specific document, the easiest way is to pass its ID as the first argument. The ID is being added to the entry when inserted as the id property. You can change the name of this property by setting the uniqueKey option when instantiating the database.

obj['mood'] = 'happy'
db.update(id, obj)

To update a collection of document based on a search, here is how you would do it:

var dev, devs = this.find({ job: 'dev' })

for(var i = 0, len = devs.length; i < len; i++) {
  dev = devs[i]
  dev['mood'] = 'happy'
  dev.job = 'clown'
  db.update(dev.id, dev)
}

Retrieving documents

The find method requires an object to parse and search with.

db.find({ mood: 'happy' })
db.find({ job: 'dev', age: 22 })

Retrieving all documents

You can either call the findAll method which returns all existing documents in the database:

db.findAll()

Or you can call the find method with no arguments, which basically does the same thing:

db.find()

Deleting a document

If you want to delete a specific document, the easiest way is to pass its ID to the function. The ID is being added to the entry when inserted as the id property. You can change the name of this property by setting the uniqueKey option when instantiating the database.

db.delete(id)

If you want to delete a collection of documents based on a search, you can pass an object to the function. The function will first perform a find, then delete all the returned documents.

db.delete({ job: dev })

How Do You Build Your Own Driver?

The thing is you don't have to use the StorageDriver I built if you don't want to use DOM Storage. I kept it out of the core so you build use your own driver as long as it relies on a key/value storage system. To build your own, it is quite easy:

(function ( exports ) {
  'use strict';

var NameOfYourDriver = function ( conf ) {
    this.conf = exports.extend({
      name: 'NameOfYourDriver'
      // whatever you need
    }, conf || {});
  };

  NameOfYourDriver.prototype.setItem = function ( key, value ) {
    // Set an item
    // If key doesn't exist, create it
    // If key exists, replace with new value
  };

  NameOfYourDriver.prototype.getItem = function ( key ) {
    // Return the item matching key 
    // If key doesn't exist, return null
  };

  NameOfYourDriver.prototype.removeItem = function ( key ) {
    // Remove the item at key if it exists
  };

if (exports.Database) {
    exports.Database.drivers.NameOfYourDriver = NameOfYourDriver;
  }
}) ( window );

Then to use it, simply instantiate the Database with an instance of your driver:

var db = new Database({
  name: 'MyDatabase',
  driver: new Database.driver.NameOfYourDriver({
    name: 'MyDatabase'
    // whatever is needed
  })
})

Done! You don't have to change the Database code at all. If you've made your driver correctly, everything should work like a charm. Pretty neat, isn't it? :)

What Next?

Well folks, you tell me! I'd like to implement a couple of other tools like limit(), sort() as long as operators like OR and AND but I'm afraid it adds too much complexity to such a simple API.

In any case if you come across a bug or think of a feature that could make this API better, make sure to open an issue on the GitHub repository.

Hugo Giraudel

About Hugo Giraudel

Front-developer from France. Author at Codrops, helper at CSS-Tricks, curator of Browserhacks. CSS Goblin, Sass hacker, margin psycho. You can catch me on HugoGiraudel.com or Twitter.

Recent Features

  • By
    fetch API

    One of the worst kept secrets about AJAX on the web is that the underlying API for it, XMLHttpRequest, wasn't really made for what we've been using it for.  We've done well to create elegant APIs around XHR but we know we can do better.  Our effort to...

  • By
    JavaScript Promise API

    While synchronous code is easier to follow and debug, async is generally better for performance and flexibility. Why "hold up the show" when you can trigger numerous requests at once and then handle them when each is ready?  Promises are becoming a big part of the JavaScript world...

Incredible Demos

Discussion

  1. Pretty neat. I like the modular driver. Thanks for it Hugo !
    By the way, whenever you want to compare a length > 0, you can type:

    if (collection.length) {
        // your stuff
    }
    
  2. If you could e mail me with a few hints on how you made your blog look this great, I would be grateful.

  3. It could be great if you move the Database Class to TypeScript!

  4. Noah Jerreel Guillen

    Can we store blobs in the indexed database? :)

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!