Edit on Match 30, 2017: Unfortunately, the Ionic folks decided to not ship the final version of this service. IonicDB is no more.
Today marks the launch of a new Ionic service, IonicDB. For those who fondly remember Parse (thanks again, Facebook), this will come as welcome news. IonicDB is a simple data storage system. It lets you store data in the cloud for your mobile apps and skip building a server just to handle simple data CRUD. It also ties nicely with the Ionic Auth system and has the ability to listen to changes to your data in real time.
As always - the docs should be the first place you go, but I've been playing with this service for a little while now and have some demos that may help get you started. Everything I'm going to show below may be found in my GitHub repo of Cordova examples so you'll be able to get the full source.
A Simple Example
To begin, ensure you've followed the directions for working with Ionic Cloud in general. This is the prereq for using any of the fancy Cloud-based services. This requires a login with Ionic so if it is the first time you've used Ionic Cloud you'll need to set that up one time.
After you've set up Ionic Cloud for your app, the DB instructions tell you this:
Once you’ve created a database for your app in our dashboard you can start storing and accessing data in your database.
So to be clear, you need to get out of code for now, and go to the Ionic Apps site (https://apps.ionic.io), find your app, and then enable Ionic DB:
Click this nice obvious button and your database will be prepared for your app. This will take you into the dashboard view:
There's a lot of useful info here as well as a look into your stats (which should be pretty boring in a new app), but let me call out a few things in particular.
Number one - collections are simply database tables. Ok, not tables, it's all NoSQL and shiny, but basically, database tables. For those who didn't grow up writing SQL for back end apps, then just think of these as buckets for your data. If your app works with cats and dogs, then you could have two collections. Really this will be up to you and your app's needs.
Now look at the toggles. They are all pretty self-explanatory I think. The third one, "Auto Create Collections", is a setting Parse had as well. Basically, while you work on your app, you may be defining new collections as you build it out. ("Oh, the client wants to support cats, dogs, and mules? Ok - let's add a new collection for mules.") But most likely, when you've gone line, you do not want new collections created. So you'll toggle this setting off. The first two refer to who can use the database and what particular rules apply - and again, this will be based on your app.
So what does the code look like? As I said, I'm not going to repeat every part of the docs - the quick start gives you an idea of what you need to do. Basically you connect and then go to town. You've got full CRUD for working with data, but as I said earlier, you also have the ability to listen for new data live, and I really like how easy the service makes this. While the quick start shows you a code snippet, I've built a full (but simple) app that shows everything working together. Let me show you how it works, and then I'll share the code.
The one page app consists of a list of Notes, which comes from IonicDB, and a simple text field to add new ones:
As you can imagine, typing a message and hitting the button adds a new one. Where things get interesting is when you work with multiple clients. Here's a video showing this in action:
Ok, so let's look at the code. First, the view, even though it's rather simple.
<ion-header>
<ion-navbar>
<ion-title>
Ionic DB Example
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-item>
<ion-label>Message</ion-label>
<ion-input type="text" [(ngModel)]="message"></ion-input>
</ion-item>
<button ion-button block (click)="sendMessage()">Send</button>
<ion-list>
<ion-item *ngFor="let chat of chats">
<h2>{{chat.text}}</h2>
<p>Posted {{chat.created | date}} at {{ chat.created | date:'shortTime' }}</p>
</ion-item>
</ion-list>
</ion-content>
Basically just a form field and button for adding data and a simple ion-list for displaying it. The component code is where the fun is:
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Database } from '@ionic/cloud-angular';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
public chats: Array<string>;
public message:string = '';
constructor(public navCtrl: NavController, public db:Database) {
this.db.connect();
this.db.collection('chats').order('created','descending').watch().subscribe( (chats) => {
console.dir(chats);
this.chats = chats;
}, (error) => {
console.error(error);
});
}
sendMessage() {
this.db.collection('chats').store({text:this.message, created:Date.now()});
}
}
The first important bit is the import up top - that's fairly self-explanatory. I've created an array of chats representing the data that will be displayed in the app. In the constructor, we connect, then open a collection called chats. I order the data by the created property, then watch and subscribe to the result. That's it for getting data and knowing when new crap is added.
Adding data is done via store, and being all fancy and NoSQL, I literally just pass an object representing my data and IonicDB stores it. As with most NoSQL-ish solutions, the 'free form' thing is awesome, but you probably want to have a good idea what your data looks like and try to be as consistent as possible.
And that's it! Nice and simple, right? Let's take a look at the collections for this app in the dashboard.
As you can see, Users is in there by default even if we aren't doing anything with security. Now let's look at chats:
As you can see, you've got some meta data for the collection as a whole, and the ability to Add/Edit/Delete right from the tool. This is cool because while most data may be user generated, you may have data that is read only (and yes, the security model supports that). In that case, you would be able to use the dashboard to set up your data easily. Notice too you can do ad hoq queries here to search for data.
An Example with Security
So what about security? IonicDB has a pretty deep security model, and to be honest, I find it a bit confusing at time. For my second demo, I had the help of Nick Hyatt from the Ionic team. I think it's going to take a few apps before I feel very comfortable with it, but I was able to build a complete demo of at least one example of it.
Given the previous demo where folks made notes and everyone could see them - how would you modify the code so that notes where user specific and only visible to the users that created them?
You begin by making use of the Auth system. (If you've never seen that, see my blog post: An example of the Ionic Auth service with Ionic 2). Then, edit your database settings in the dashboard to require authenticated users. That was the first toggle in dashboard seen above.
Once you've done that, you then need to modify your code a bit. The first change you make is to app.module.ts, in your cloudSettings object:
const cloudSettings: CloudSettings = {
'core': {
'app_id': '1f06a1e5'
},
'database': {
'authType': 'authenticated'
}
};
If you try to connect to the database without being logged in, you'll get an error. But here's a nit - Ionic's Auth system caches logins. So you need to handle this in two places - the application startup, and your login/register routine. So here's my app.component.ts:
import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';
import { HomePage } from '../pages/home/home';
import { LoginPage } from '../pages/login/login';
import { Auth, Database } from '@ionic/cloud-angular';
@Component({
templateUrl: 'app.html'
})
export class MyApp {
rootPage;
constructor(platform: Platform, public auth:Auth, public db:Database) {
platform.ready().then(() => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
StatusBar.styleDefault();
Splashscreen.hide();
if(this.auth.isAuthenticated()) {
this.db.connect();
this.rootPage = HomePage;
} else {
this.rootPage = LoginPage;
}
});
}
}
As you can see, if I detect a cached login, I connect to my database before sending the user to the appropriate page. By the same token, the login page has logic for both logging in as an existing user, and registering, and in both of those cases, I also need to connect to my database. I won't share the entire file as it's really a small mod from the Auth blog post I linked above (and again, I'll be sharing links to the full source code), but here is just the part that handles login:
doLogin() {
if(this.showLogin) {
console.log('process login');
if(this.email === '' || this.password === '') {
let alert = this.alertCtrl.create({
title:'Register Error',
subTitle:'All fields are rquired',
buttons:['OK']
});
alert.present();
return;
}
let loader = this.loadingCtrl.create({
content: "Logging in..."
});
loader.present();
this.auth.login('basic', {'email':this.email, 'password':this.password}).then(() => {
loader.dismissAll();
//this is crucial
this.db.connect();
this.navCtrl.setRoot(HomePage);
}, (err) => {
loader.dismissAll();
console.log(err.message);
let errors = '';
if(err.message === 'UNPROCESSABLE ENTITY') errors += 'Email isn\'t valid.<br/>';
if(err.message === 'UNAUTHORIZED') errors += 'Password is required.<br/>';
let alert = this.alertCtrl.create({
title:'Login Error',
subTitle:errors,
buttons:['OK']
});
alert.present();
});
} else {
this.showLogin = true;
}
}
You can see the connection in the success handler for logging in.
So far so good. The next change is to where we work with data. In both are "get notes" and "add note" logic, we want to filter by the current user. Instead of sharing the entire page, here are those mods, first to "get notes" part:
this.db.collection('chats').findAll({creator:this.user.id}).order('created','descending').watch().subscribe( (chats) => {
Pretty simple, right? Basically filter by property creator and use my current login id as the value. Here is the new save routine:
this.db.collection('chats').store({text:this.message,
created:Date.now(), creator:this.user.id});
Now we add a third property, creator, to mark our content. So we're not quite done yet. While we've done the right thing in the app, we also need to lock down the server as well. That requires permissions, so back in the dashboard, enable that second toggle as well.
At this point, everything is blocked. So to be clear, if I try to use my app, I won't be allowed to do anything with the data. I can connect, but I can't CRUD a lick of content. I'll begin by enabling the user to create content. In my dashboard, under permissions, I'll add a new permission:
I gave it a name of 'create', told it the collection, and then defined the permission rule. Basically - "when we run a store call, text and created can be anything, but the creator must be me."
Next I need to allow myself the ability to read my content:
Whew! And that's it. Remember, by enabling permissions, we've automatically closed down every other single operation, so we don't have to worry about locking down deletes or anything else.
As an aside, if you wanted to build a Twitter-like client where everyone's notes were shared, you would simply modify that
read permission like so: findAll({owner: any())
(And again, thank you, Nick!)
For the full source code, here are the two app:
https://github.com/cfjedimaster/Cordova-Examples/tree/master/db1 https://github.com/cfjedimaster/Cordova-Examples/tree/master/db2
Let me know if you have any questions (although be gentle, I'm still learning this myself), and let me know what you think!