TaffyDB - The JavaScript Database

TaffyDB is an opensouce library that brings database features into your JavaScript applications.

Introduction

How you ever noticed how JavaScript object literals look a lot like records? And that if you wrap a group of them up in an array you have something that looks a lot like a database table? TaffyDB is a libary to bring powerful database funtionality to that concept and rapidly improve the way you work with data inside of JavaScript.

What makes it sticky

  • Extremely fast
  • Powerful JavaScript centric data selection engine
  • Database inspired features such as count, update, and insert
  • Robust cross browser support
  • Easily extended with your own functions
  • Compatible with any DOM library (jQuery, YUI, Dojo, etc)

Creating a JavaScript Database

Let's create a database containing a collection of posts one might find on service like Twitter or Facebook.

var newsfeed = TAFFY([
	{"id":1,"user":"kmart77","stars":3,"text":"JavaScript is a meal best served cold"},
	{"id":2,"user":"lukeisyourfather","stars":5,"text":"Anyone want to meet tonight at Fado?"},
	{"id":3,"user":"bemine","stars":3,"text":"I reallylike the new prirotity inbox #gmail"},
	{"id":4,"user":"keepit","stars":3,"text":"Laughing at this weeks fails video."},
	{"id":5,"user":"piegirl22","stars":3,"text":"It is impossible to change your clothes in a public bathroom. Ick."},
	{"id":6,"user":"kmart77","stars":3,"text":"@kmart77: Have fun in New York!"},
	{"id":7,"user":"justinb","stars":5,"text":"Please set the noise level to earthquake"}
]);

Filtering for data

Now we have a TaffyDB collection containing the JSON objects (records) we passed in. Using the extensive API we can start accessing and working with our data.

newsfeed().count() // returns 7
newsfeed.sort("user") // sort records by username

But the real power comes into play once we start filtering for records. Think of this filtering as using data selectors (if you know jQuery) or building the a database query "where" clause (if you know databases).

newsfeed({user:"kmart77"}).count() // returns 1
// filter for posts where user is equal to kmart77

Keeping this JavaScript centric, we use an object literal to do the record lookup. No ugly string concatenations here.

Now that we understand basic filters lets look at some of the options we have for filtering

newsfeed({text:{ends:"#gmail"}}).count() // returns 1
// filter for posts where text ends with #gmail
// you can also use starts

newsfeed({stars:{gt:3}}).count() // returns 2
// filter for posts where the star rating is greater than 3

There are a lot more types of filtering including options to ignore case, use functions or regular expressions, and traverse objects within your database.

Working with data

When you filter for data you create a query. If you assign this query to a variable you can pass it around. It is not static and the data will be updated if other logic changes the underling records.

var kmart77 = newsfeed({user:"kmart77"});
// kmart77 is now a first class variable and can be passed to functions

So now that you have your query lets look at some of the methods you can use to work with the data it has access to.

kmart77.each(function (r) {
    alert(r.text);
}) // for each record, call the function you've provided and alert the text of the post
kmart77.select(text) // returns an array with the text of each post

var kmart77_5Star = kmart77.filter({stars:5}); // apply another filter

kmart77_5Star.count() // returns 0
kmart77.count() // still returns 1

Modifying data

Let's make a basic change to a record:

newsfeed({id:"1"}).update({stars:5});
// updates the star rating, returns a query object

Now let's insert another post:

newsfeed.insert({"id":8,"user":"justinb","stars":3,"text":"Have a great weekend everyone!"});
//inserts a new record

Finally let's do a remove:

newsfeed({user:"kmart77"}).remove();
//removes all records for user kmart77

Downloads

Get it on github.

CDNs

Coming soon.

Beginner's Guide

TaffyDB is very easy to get started with. This brief turtoial will introduce you to a few of the core concepts and should be enough to get started even if you aren't already a web developer.

Step 1: Creating a quick sandbox

It is recmmended that you download and use your own copy of TaffyDB, but for the purposes of playing around you can use the latest version right off of GitHub. If you have never created a web page before you can simply create a text document, include the code below, and save it to your desktop as test.html.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
	</head>
	<body>
	</body>
				
</html>

The script tag imports TaffyDB right onto the page.

Step 2: Your First DB

Let's create a DB to store some city information. You'll need another script tag and you'll be writing JavaScript.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY();

		</script>
	</head>
	<body>
	</body>
				
</html>

Step 3: Adding records

You can add records on creation and via the .insert() method. Below we are doing both.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY([{name:"New York",state:"WA"},{name:"Las Vegas",state:"NV"},{name:"Boston",state:"MA"}]);
	
		cities.insert({name:"Portland",state:"OR"});

		</script>
	</head>
	<body>
	</body>
				
</html>

Step 4: Your first query

Our city db is itself a function that accepts a filter object (an object that will be matched to records). Let's query for Boston. We will alert the .count() method to see our results.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY([{name:"New York",state:"WA"},{name:"Las Vegas",state:"NV"},{name:"Boston",state:"MA"}]);
	
		cities.insert({name:"Portland",state:"OR"});

		alert(cities({name:"Boston"}).count());

		</script>
	</head>
	<body>
	</body>
				
</html>

Step 5: Chaining methods

You can chain together. Below we will get the first two records from the DB and use the .each() method to alert their names.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY([{name:"New York",state:"WA"},{name:"Las Vegas",state:"NV"},{name:"Boston",state:"MA"}]);
	
		cities.insert({name:"Portland",state:"OR"});

		cities().limit(2).each(function (r) {alert(r.name)});

		</script>
	</head>
	<body>
	</body>
				
</html>

Step 6: Updating data

You may have noticed that New York is in the wrong state. Let's fix that and alert the new state.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY([{name:"New York",state:"WA"},{name:"Las Vegas",state:"NV"},{name:"Boston",state:"MA"}]);
	
		cities.insert({name:"Portland",state:"OR"});

		cities({name:"New York"}).update({state:"NY"});

		alert(cities({name:"New York"}).first().state);


		</script>
	</head>
	<body>
	</body>
				
</html>

Step 7: Sorting

Sorting is a big part of TaffyDB using the .order() method.

<html>
	<head>
		<script src="https://github.com/typicaljoe/taffydb/raw/master/taffy.js"></script>
		<script>

		var cities = TAFFY([{name:"New York",state:"WA"},{name:"Las Vegas",state:"NV"},{name:"Boston",state:"MA"}]);
	
		cities.insert({name:"Portland",state:"OR"});

		alert(cities().order("name").first().name);

		</script>
	</head>
	<body>
	</body>
				
</html>

What's next

There are a lot of other methods you can use against your data. Some highlights include .remove() to delete records, .get() to get an array of records, and .supplant to merge records with a string template. Continue to browse the docs for more ways to use TaffyDB.

Mailing List

Get your questions answered. Tell us what you have built. Help others leverage TaffyDB.

Subscribe to TaffyDB Mailing List

Extending TaffyDB

TaffyDB is easy to extend. Simply use the extend method to add a new method to all TaffyDB collections on your page.

Here is an example that creates an "avg" method that takes a column and returns the avg value.

// Create a new empty database
TAFFY.extend("avg",function (c) {
	// This runs the query or returns the results if it has already run
	this.context({
           results: this.getDBI().query(this.context())
    });
    // setup the sum
    var total = 0;
    // loop over every record in the results and sum up the column.
    TAFFY.each(this.context().results,function (r) {
		total = total + r[c];
    })
    
    // divide the total by the number of records and return
    return total/this.context().results.length;
});

What can you build?

Running queries against your database

The heart of TaffyDB and any database is running queries against your data. This is done after creation of your database by calling the root function and building Filter Objects.

// Create a new empty database
var db = TAFFY();

// Run a query against the DB to return all rows
db();

// Real world example - remove all records
db().remove();

Looking up individual records

Every object within a TaffyDB collection has a ___id value set by TaffyDB. This value is not intended for you to know, but is useful when building dynamic applications. This can be used to lookup a record by passing it into the root function as a string.

// Looks up a record based on its id
db("T000008R000002");

// Real world example - update records "status" column
db("T000008R000002").update({status:"Active"});

This also works if you have a copy of the whole record.

// get the first record
var firstRecord = db().first();

// look up this record again
db(firstRecord);

// Real world example - update records "status" column
db(firstRecord).update({status:"Active"});

Using functions

To give you full control over the results of your query you can always pass in a function. Just have it return true if you want the record in your final results.

// functional example, returns all records
db(function () {
	return true;
});


// Real world example - function returns records with a status of active
db(function () {
	return (this.status == "Active") ? true : false;
});

Basic queries

TaffyDB uses a very JavaScript centric Filter Object for looking up queries. There is no string concatenation and you can quickly compose these by hand or dynamically from within your app. The filter object is compared against each record using some business rules and if it passes the record remains in the results set.

The most common Filter Object is used simply to check if a column is equal to value.

// does a match for column and value
db({column:"value"});


// Real world example - records with a status of active
db({status:"Active"});

This is the short form of this

// does a match for column and value
db({column:{is:"value"}});


// Real world example - records with a status of active
db({status:{is:"Active"}});

The "is" part of this expressions can be swapped out for a variety of other comparisons as listed below.

Using !(bang)

For any comparison operator you can quote it and add a ! sign to reverse the meaning.

// does a match for column that is not a value
db({column:{"!is":"value"}});

// Real world example - records with a status other than of active
db({status:{"!is":"Active"}});

Adding additional filters

Using the almighty comma you can add additional lookups to you Filter Object.

// does a match for column that is a value and column2 is a value
db({column:"value",column2:"value"});

// Real world example - records with a status of active and a role of admin
db({status:"Active",role:"Admin"});

You can also pass in additional Filter Objects into the function

// does a match for column that is a value and column2 is a value
db({column:"value"},{column2:"value"});

// Real world example - records with a status of active and a role of admin
db({status:"Active"},{role:"Admin"});

Using arrays for IN and OR

In a database you can use "in" to pass in a collection of values to compare against. This is possible in TaffyDB via the array.

// does a match for column that is one of two values
db({column:["value","value2"]);

// Real world example - records with a status of active or pending
db({status:["Active","Pending"]});

You can also pass in an array of Filter Objects with each one being treated as a logical OR.

// does a match for column that is one of two values
db([{column:"value"},{column:"value2"}]);

// Real world example - records with a status of active or pending
db([{status:"Active"},{status:"Pending"}]);

Bringing it all together

A real world example of a complex query.

// return records where the role is Admin and the status is Active. 
// Also return records where the role is Admin, the status is Pending, and the manager_review is true

db({role:"Admin"},[{status:"Active"},{status:"Pending",manager_review:true}]);

Comparison Operators

In addition to the default "is" operator there are a lot of other operators you can use to lookup records.

is Example:
{column:{is:value}}
Used to see if a column value is equal to a supplied value.
isnocase Example:
{column:{isnocase:value}}
Used to see if a column value is equal to a supplied value. Ignores case of column and value.
left Example:
{column:{left:value}}
Used to see if the start of a column is the same as a supplied value.
leftnocase Example:
{column:{leftnocase:value}}
Used to see if the start of a column is the same as a supplied value. Ignores case of column and value.
right Example:
{column:{right:value}}
Used to see if the end of a column is the same as a supplied value.
rightnocase Example:
{column:{rightnocase:value}}
Used to see if the end of a column is the same as a supplied value. Ignores case of column and value
like Example:
{column:{like:value}}
Used to see if column contains a supplied value.
likenocase Example:
{column:{likenocase:value}}
Used to see if column contains a supplied value. Ignores case of column and value
regex Example:
{column:{regex:value}}
Used to see if column matches a supplied regular expression.
lt Example:
{column:{lt:value}}
Used to see if column is less than a supplied value.
lte Example:
{column:{lte:value}}
Used to see if column is less than or equal to a supplied value.
gt Example:
{column:{gt:value}}
Used to see if column is greater than a supplied value.
gte Example:
{column:{gte:value}}
Used to see if column is greater than or equal to a supplied value.
has Example:
{column:{has:value}}
Used to see if column that is an object has a value or object appearing in its tree.
hasAll Example:
{column:{hasAll:value}}
Used to see if column that is an object has a value or object appearing in its tree.
isSameArray Example:
{column:{isSameArray:value}}
Used to see if column is an array and is the same as a supplied array.
isSameObject Example:
{column:{isSameObject:value}}
Used to see if column is an object and is the same as a supplied object.
isString Example:
{column:{isString:true}}
Used to see if column a string.
isNumber Example:
{column:{isNumber:true}}
Used to see if column a number.
isArray Example:
{column:{isArray:true}}
Used to see if column an array.
isObject Example:
{column:{isObject:true}}
Used to see if column an object.
isFunction Example:
{column:{isFunction:true}}
Used to see if column a function.
isFunction Example:
{column:{isFunction:true}}
Used to see if column a function.
isBoolean Example:
{column:{isBoolean:true}}
Used to see if column a boolean (true/false).
isNull Example:
{column:{isNull:true}}
Used to see if column null.
isUndefined Example:
{column:{isUndefined:true}}
Used to see if column undefined.

Creating your database

You use the global TAFFY function to create a new database.

// Create a new empty database
var db = TAFFY();

// Create a new database a single object (first record)
var db = TAFFY({record:1,text:"example"})

// Create a new database using an array
var db = TAFFY([{record:1,text:"example"}])

// Create a new database using a JSON string
var db = TAFFY('[{"record":1,"text":"example"}]')

DB Queries

Once you have a database you'll be able to call your variable as function. This will setup a query and return a collection of methods to work with the results of that query.

db() Takes:
One or more Strings, Records, A Filter Objects, Arrays, or Functions.
Returns:
A Query Object
Used to query the database and return a method collection to start working with the data.
db(); // returns all rows

DB Methods

In addition to the query function you have a couple of top level methods to use.

db.insert() Takes:
An object or array of objects/json strings to insert.

Optional: Call Event override
true runs onUpdate event (default)
false does not onUpdate event
Returns:
A query pointing to the inserted records
Inserts records into the database.
db.insert({column:"value"}); // inserts one record
						
db.insert({column:"value"},false); // insert but do not call onInsert event
db.order() Takes:
A string listing columns to order by separated by commas.
Optional: Use asec, desc, logical, and logicaldesc to influencence sort direction.
Returns:
true
Sorts the db by column. Note that logical sorting is default.
Note: db.sort() is different than db().order() in that sort() changes the order of the records in the database while order() only impacts the order of results returned from a query.
db.order("columnname"); // orders collection by columnname
db.store() Takes:
A string with the storage name for the DB
Returns:
true if data was returned from localStorage, false if no data returned
Sets up a localStorage collection for a given name. If data exists already the data will be loaded into the collection. Changes are auto synced to localStorage.

Pass in false to terminate storing the collection (data in localstorage will be unchanged).

Note: localStorage is not avaliable in all browsers. If localStorage is not found then .store() will be ignored.
db.store("name"); // starts storing records in local storage
db.settings() Takes:
An object.
Returns:
The settings object
Used to modify the following settings on each DB:
  • template: this is a template object {} that is merged with each new record as it is added
  • forcePropertyCase: (defaults null) Can be "lower" or "higher" and forces properties to a case on insert, update.
  • onInsert: function to run on record insert. The this keyword will be the new record.
  • onUpdate: function to run on record update. The this keyword will be the new record. Arg 1 will be old record. Arg 2 will be the changes.
  • onRemove: function to run on record remove. The this keyword will be the removed record.
  • onDBChange: function to run on change to the DB. The this keyword will be the entire DB.
  • cacheSize: size of cached query collection. Default to 100. Set to 0 to turn off caching.
db.settings({template:{show:true}}); // sets the template to have a show value set to true by default on all records

db.settings({onUpdate:function () {alert(this)}}); // sets the onUpdate event

					

Query Object Methods

Any call to data collections root function will return a new query object. The methods below can be used to interact with the data.

db().update() Takes:
Any:
An object to be merged with the matching objects.
A function that will return the modified record for update.
Two arguments: a string column name and a value to set the column to

Optional Call Event override
true runs onUpdate event (default)
false does not onUpdate event
Returns:
Query Object
Updates the records in the query set.
db().update({column:"value"}); // sets column to "value" for all matching records

db().update("column","value"); // sets column to "value" for all matching records

db().update(function () {this.column = "value";return this;}); // sets column to "value" for all matching records


db().update({column:"value"},false); // update but do not call onUpdate event
db().remove() Takes:

Optional Call Event override
true runs onUpdate event (default)
false does not onUpdate event
Returns:
Count of removed records.
Removes records from the database
db().remove(); // removes all matching records from the database
						
db().remove(true); // removes but do not call onRemove event
db().filter() Takes:
One or more Strings, Records, A Filter Objects, Arrays, or Functions.
Returns:
Query Object
See Filtering
Applies a sub filter and returns a new Query Object to let you access the results of the filter.
db().filter({column:value});
						
db().filter({column:value}).count();

collection({column:{gte:value}}).filter({column:{lte:value}}).count();
					
db().order() Takes:
A string listing columns to order by separated by commas.
Optional: Use asec, desc, logical, and logicaldesc to influence sort direction.
Returns:
Query Object
Sorts the query results by column based. Note that logical sorting is default. Note: db.sort() is different than db().order() in that sort() changes the order of the records in the database while order() only impacts the order of results returned from a query.
db().order("col1"); // sorts by col1 using a logical sort

db().order("col1 asec, col2 asec"); // sorts by col1 then col2

db().order("col1 desc"); // sorts by col1 descending

db().order("col1 logicaldesc"); // sorts by col1 using a logical sort descending
					
db().limit() Takes:
A number.
Returns:
Query Object
Limits the number of results in a query set.
db().limit(15); // Limits returned results to first 15
db().start() Takes:
A number.
Returns:
Query Object
Sets the starting record number. Used for offset and paging.
db().start(15); // Starts returning results at record 15
db().each() Takes:
A function.
Returns:
Query Object
Runs one or more functions once for every record in the query.
db().each(function (record,recordnumber) {
	alert(record["balance"]);
}); // alerts the value of the balance column for each record
db().map() Takes:
A function.
Returns:
An array of results from your function being run against the records in the query.
Runs your function against each record in the query and returns the results as an array.
db().map(function (record,recordnumber) {
	return record.balance*0.2;
}); // returns an array of numbers that are each 20% of the actual balance for each record
db().callback() Takes:
A function and an (optional) delay.
Used for lazy query execution and to prevent blocking. Provided function will be called with current Quote Object after query has run in a setTimeout block.
db().callback(function () {
    alert(this.count()); // alert count of matching records					
});
db().get() Returns:
An array of all matching records.
Prefered method for extracting data. Returns an array of matching records. Also used for exporting records form the database.
db().get(); // returns an array of all matching records
db().stringify() Returns:
A JSON representation of an array of all matching records.
Used for exporting data as JSON text. Returns a JSON array filled with JSON objects for each record.
db().stringify(); // returns a JSON array of all matching records
db().first() Returns:
A single record.
Returns the first record in a record set.
db().first(); // returns the first record out of all matching records.
db().last() Returns:
A single record.
Returns the last record in a record set.
db().last(); // returns the last record out of all matching records.
db().sum() Takes:
One or more column names.
Returns:
A number.
Returns the sum total of the column or columns passed into it.
db().last("balance"); // returns the sum of the "balance" column.
db().min() Takes:
Column name.
Returns:
A number or value.
Returns the min value for the column passed in.
db().min("balance"); // returns the lowest value of the "balance" column.
db().max() Takes:
Column name.
Returns:
A number or value.
Returns the max value for the column passed in.
db().max("balance"); 
// returns the highest value of the "balance" column.
db().select() Takes:
One or more column names.
Returns:
For one column: An array of values.
For two or more columns: An array of arrays of values.
Used to select columns out the database. Pass in column names to get back an array of values.
db().select("charges","credits","balance"); 
// returns an array of arrays in this format [[-24,20,-4]]

db().select("balance"); 
// returns an array of values in this format [-4,6,7,10]
					
db().distinct() Takes:
One or more column names.
Returns:
For one column: An array of values.
For two or more columns: An array of arrays of values.
Used to select distinct values from the database for one or more columns. Pass in column names to get back an array of distinct values.
db().distinct("title","gender"); 
// returns an array of arrays in this format [["Mr.","Male"],["Ms.","Female"]]

db().distinct("title"); 
// returns an array of values in this format ["Mr.","Ms.","Mrs."]
db().supplant() Takes:
A string template.
Optional return array flag.
Returns:
Defaults to a string. If return array flag is true then an array of strings with an entry for each record.
Used to merge records with a template formated with {key} values to be replaced with real values from the records.
db().supplant("<tr><td>{balance}</td></tr>"); 
// returns a string in this format "<tr><td>-4</td></tr><tr><td>6</td></tr>"

db().supplant("<tr><td>{balance}</td></tr>",true); 
// returns an array of strings format ["<tr><td>-4</td></tr>","<tr><td>6</td></tr>"]