A Weekend With GUN
Graph storage using gunDB
Looking for alternatives to Firebase, I stumbled upon gunDB (check it out at http://gun.js.org/). It does all sorts of cool stuff but to sum it up:
- Key/Value, Document, Relational or Graph structure
- Realtime
- Decentralized
- Offline first
To start messing around with gunDB you can use this jsbin as your gunDB playground. You can follow along by typing or copying each snippet into the JavaScript section and then running it to see your results in the console.
This post refers to gunDB version 0.3.4 which is the latest release at the time of this writing.
How it works
I like the graph aspect of gunDB so I’ll be covering how that works specifically but the same functions work regardless of what structure you go with.
After importing/requiring the gun library you do:
var gun = Gun();
You’re good to go to test it out. Your data won’t actually be stored anywhere other than local storage in the browser right now, but check out how to set up peers, store to a file and connect to S3 here. I will just be covering how to interact with gun.
Here are the three key functions you’ll use when working with gunDB:
var obj = gun.get('path/to/object') .path('property')
.put(object);
At the end of each call you end up in a context where you can make more calls. In the above example we got the object stored at ‘path/to/object’ then we went to its property ‘property’ and put an object there. This makes ‘path/to/object’ ‘s property ‘property’ equal to object.
Here’s an actual example:
var saad = gun.get('user/1')
.path('name')
.put({first: "Saad", last:"Elbeleidy"});// Which is also the same asgun.get('user/1')
.put({name: {first: "Saad", last:"Elbeleidy"}});
“put” updates or adds properties to your specified context so this would result in the same outcome:
var saad = gun.get('user/1')
.path('name')
.put({first: "Saaaa", last: "Elbeleidy"}); // Oh no! A typo!saad.path('name')
.put({first: "Saad"}); // Fixed it!
To see what’s in your current context you can use “val”
saad.val(function(value){
console.log(value);
}); // outputs the user object at 'user/1'
Now let’s create a relationship. I’m currently learning gunDB so let’s add gunDB into the DB and then connect it to me.
// First let's add our gun project
var gunDB = gun.get('projects/gunDB')
.put({name: "gunDB", site: "http://gun.js.org/", repo: "https://github.com/amark/gun"});saad.path('learning')
.put(gunDB);
Now if I want to see what I’m learning I can just do:
saad.path('learning').val(function(value){
console.log(value);
}); // equivalent to gunDB.val()
Two issues arise here:
- What if I’m learning multiple things?
- What if I want to get to var saad from var gunDB?
Answering the first question: One To Many relationships
“put” creates a One to One, one way referencing relationship. This means that the properties of the object I’m putting go straight into the path that I selected (One to One) and that the object doesn’t have a way to get to the parent (one way referencing).
To get a One To Many relationship we use “set”:
var projects = [gundb, firebase, polymer,pagekit]; // gun refs projects.forEach(function(project){
saad.path(‘learning’).set(project);
});// All output the same thing, pick your preferencesaad.path('learning').val(function(value){
console.log(value);
}); // Should output a list of 4 objects
“set” creates a One to Many, one way referencing relationship. This means that the properties of the object I’m putting go into the path that I selected as one element of a list(One to Many) and that the object doesn’t have a way to get to the parent (one way referencing).
Now that we have the relationship connected, you’re probably thinking, how do I check the value of each object in the entire set. Well instead of just using “val” we can use it in combination with “map”:
saad.path('learning').map().val(function(value){
console.log(value);
}); // Should output each object
Answering the second question: Two way referencing relationships
A two way referencing relationship is just two one way referencing relationships. So, right now that’s just what we do. If we want to reference the parent we just go to the property we want to use in the child and add the parent reference.
So here are the 4 cases:
I can learn one project and the project can only be learned by one person (One to one)
// A one to one, two way referencing relationship
saad.path('learning').put(gunDB).path('isLearnedBy').put(saad);
I can learn many projects and each project can only be learned by one person (One to many)
// A one to many, two way referencing relationship
saad.path('learning').set(gunDB).path('isLearnedBy').put(saad);
I can learn one project and the project can be learned by multiple people (Many to one)
// A many to one, two way referencing relationship
saad.path('learning').put(gunDB).path('isLearnedBy').set(saad);
I can learn many projects and each project can be learned by multiple people (Many to many)
// A many to many, two way referencing relationship
saad.path('learning').set(gunDB).path('isLearnedBy').set(saad);
To summarize:
// A one to one, two way referencing relationship
saad.path('learning').put(gunDB).path('isLearnedBy').put(saad);// A one to many, two way referencing relationship
saad.path('learning').set(gunDB).path('isLearnedBy').put(saad);// A many to one, two way referencing relationship
saad.path('learning').put(gunDB).path('isLearnedBy').set(saad);// A many to many, two way referencing relationship
saad.path('learning').set(gunDB).path('isLearnedBy').set(saad);
Now that we see the syntax for connecting things it makes sense to do a full example. Unfortunately this example doesn’t have any one to one relationships but it should still be beneficial to go through:
Blog
Here’s an example on how to structure a blog’s data structure using gunDB. We will start with 4 object types:
- User
- Post
- Tag
- Comment
Here’s how they are all connected:
- User to Post — One To Many
A user can have many posts but a post belongs to only one user. - User to Comment — One To Many
A user can have many comments but a comment belongs to only one user. - Post to Tag — Many To Many
A post can have many tags and a tag can be applied to many posts. - Post to Comment — One To Many
A post can have many comments but a comment can only apply to one post.
Creating our users
var markInfo = {
name: “Mark”,
username: “@amark”
};
var jesseInfo = {
name: “Jesse”,
username: “@PsychoLlama”
};
var mark = gun.get(‘user/’+markInfo.username).put(markInfo);
var jesse = gun.get(‘user/’+jesseInfo.username).put(jesseInfo);
Creating a post
The Post object:
var lipsum = {
title: “Lorem ipsum dolor”,
slug: “lorem-ipsum-dolor”,
content: “Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.”
};
Some tags
var lorem = {
name: “lorem”,
slug: “lorem”
};
var ipsum = {
name: “ipsum”,
slug: “ipsum”
};
var dolor = {
name: “dolor”,
slug: “dolor”
};
Lets store them!
var lipsumPost = gun.get(‘post/’+lipsum.slug).put(lipsum);var loremTag = gun.get(‘tag/’+lorem.slug).put(lorem);
var ipsumTag = gun.get(‘tag/’+ipsum.slug).put(ipsum);
var dolorTag = gun.get(‘tag/’+dolor.slug).put(dolor);
Let’s connect them!
First, we connect the post to its author, then we connect all the tags:
lipsumPost.path(‘author’).put(mark).path(‘posts’).set(lipsumPost)
.path(‘tags’).set(loremTag).path(‘posts’).set(lipsumPost)
.path(‘tags’).set(ipsumTag).path(‘posts’).set(lipsumPost)
.path(‘tags’).set(dolorTag).path(‘posts’).set(lipsumPost);
Adding a comment
The comment & its connections
var sit = {
id: 1,
text: “Sit amet, consectetur adipiscing elit.”
};var sitComment = gun.get(‘comment/’+sit.id).put(sit);sitComment.path(‘author’).put(jesse).path(‘comments’).set(sitComment)
.path(‘post’).put(lipsumPost).path(‘comments’).set(sitComment);
Checking it all works
If you want to check that everything worked fine, you can log some of the references’ values to check.
mark.val(function(v){console.log(v);});
mark.path(‘posts’).map().val(function(v){console.log(v);});jesse.val(function(v){console.log(v);});
jesse.path(‘comments’).map().val(function(v){console.log(v);});lipsumPost.path(‘comments’).map().val(function(v){console.log(v);});
lipsumPost.path(‘author’).val(function(v){console.log(v);});
lipsumPost.path(‘tags’).map().val(function(v){console.log(v);});loremTag.path(‘posts’).map().val(function(v){console.log(v);});sitComment.path(‘author’).val(function(v){console.log(v);});
sitComment.path(‘post’).val(function(v){console.log(v);});
Having some fun
// Circular references + chaining = fun
mark.path('posts').map()
.path('tags').map()
.path('posts').map()
.path('comments').map()
.path('author'); // All the authors of the comments on the posts with the tags of the // posts that mark has posted. (Say that 5 times real fast)// Same thing, explained
mark.path('posts').map() // All of mark's posts
.path('tags').map() // All of mark's posts' tags
.path('posts').map() // All of the tags' posts
.path('comments').map() // All of the posts' comments
.path('author'); // All of the comments' authors// Based on our inserted dataset this would return jesse
If you want to test this out yourself and maybe change a thing or two, check out the code here.
And there you go, a short intro to GUN’s gunDB. If you got this far, you probably want to start working with gun. You can do that on your project by simply running:
npm install gun
Enjoy!
P.S. Huge thanks to the awesome Mark Nadal and the folks over in the GUN gitter chat for being so helpful and putting up with all of my questions. Get in touch with them for any questions on working with gunDB.
I hope you enjoyed that and would appreciate your feedback. Let me know what you think!