IonicDB

Ionic Cloud has been deprecated. To continue using Ionic services, migrate to Ionic Pro before Jan. 31, 2018.

Summary

Apps need data. They need to store it, find it, and know when it changes. We provide real-time data storage that works with our Auth service so that your users can safely and securely store their data and see instantly when it changes.

Features:

  • JSON style document storage
  • Query documents with abilty to watch in real-time for changes
  • Authentication and Permissions system allows for restricting read/write privileges for users

Setup

Prerequisites

In order to use IonicDB you must have the latest Cloud Client installed and configured

Quick Start

Once you’ve created a database for your app in our dashboard you can start storing and accessing data in your database.

import {Database} from '@ionic/cloud-angular';

@Component( ... )
export class MyPage {
  public chats: Array<string>;
  
  constructor(public db: Database) {
    this.db.connect();
    this.db.collection('chats').watch().subscribe( (chats) => {
      this.chats = chats;
    }, (error) => {
      console.error(error);
    });
  }

  sendMessage(message: string) {
    this.db.collection('chats').store({text: message, time: Date.now()});
  }
}

To learn how to interact with your database read the full API Documentaion.

angular.module('myapp.controllers', ['ionic.cloud'])

.controller('MyCtrl', function($scope, $ionicDB) {
  $ionicDB.connect();
  $ionicDB.collection('chats').watch().subscribe( function(chats) => {
    $scope.chats = chats;
  }, (error) => {
    console.error(error);
  });

  $scope.sendMessage = function(message) {
    $ionicDB.collection('chats').store({text: message, time: Date.now()});
  }
})

API Documentation

Connecting to your Database

In order to send data and receive real-time updates you need to establish and maintain a connection to your database.

connect()

Establish a Database connection.

Note that you can create a Collection from the db instance without calling connect() first.

// Get access to the messages collection
const messages = this.db.collection('messages');

// Establish the db connection.
this.db.connect();

// Start using the collection
messages.store({ msg: 'Hello World!' });
var messages = $ionicDB.collection('messages')

// Establish the db connection.
$ionicDB.connect();

// Start using the collection
messages.store({ msg: 'Hello world!' });

disconnect()

Close a db connection. If you are using the Auth service for authentication logging the user out with will also disconnect them from the database.

$ionicDB.disconnect();
this.db.disconnect();

status()

Receive status updates about the connection to the db server.

status() returns an RxJS Observable which emits updates about connection status.

The emitted status objects can be one of the following:

There are also shortcut methods for receiving only a particular type of status:

this.db.status().subscribe( (status) => {
  if (status.type === 'reconnecting') {
    // Display connecting spinner
    console.log(status.type); // reconnecting
  }
});

// Equivalent to
this.db.onReconnecting().subscribe( (status) => {
  // Display connecting spinner
  console.log(status.type); // reconnecting
});
$ionicDB.status().subscribe( (status) => {
  if (status.type === 'disconnected') {
    // Display connecting spinner
    console.log(status.type); // reconnecting
  }
});

// Equivalent to
$ionicDB.onReconnecting().subscribe( (status) => {
  // Display connecting spinner
  console.log(status.type); // reconnecting
});

Collections

At the heart of IonicDB is the notion of a Collection. A Collection is a group of related documents. Documents in a Collection are identified by a unique key stored in the id field.

Note: The id field is automatically generated for new documents if none is specified. If you choose to assign an id to a document it must be a string.

// get a handle to a Collection
const messages = this.db.collection("messages");
// get a handle to a $ionicDB.collection
var messages = $ionicDB.collection("messages");

Methods on a Collection object allow you to create, read, update and delete documents in the collection. Selections can be performed by matching on any field by passing an object to match against.

const messages = this.db.collection('messages');
// Store a message
messages.store({
    url: avatar_url,
    from: "bob",
    text: "Hello from Ionic"
});

// get the first message from Bob
messages.find({from: "bob"}).fetch().subscribe(msg => console.log(msg));

// get the message with ID "ionitron"; a "shortcut" that only works when fetching based on the
// unique document ID (the equivalent long form is `find({id: "ionitron"})`)
messages.find("ionitron").fetch().subscribe(msg => console.log(msg));

// get all messages from Bob, ordered by the created field
messages.order("created").findAll({from: "bob"}).fetch().subscribe(msg => console.log(msg));
var messages = $ionicDB.collection('messages');
// Store a message
messages.store({
    url: avatar_url,
    from: "bob",
    text: "Hello from Ionic"
});

// get the first message from Bob
messages.find({from: "bob"}).fetch().subscribe(function(msg) {
    console.log(msg);
});

// get the message with ID "ionitron"; a "shortcut" that only works when fetching based on the
// unique document ID (the equivalent long form is `find({id: "ionitron"})`)
messages.find("ionitron").fetch().subscribe(function(msg) {
    console.log(msg);
});

// get all messages from Bob, ordered by the created field
messages.order("created").findAll({from: "bob"}).fetch().subscribe(function(msg) {
    console.log(msg);
});

Read Operations

Note: All read and write return an RxJS Observable. Observable’s have an extensive API which you can read about here. For convenience we’ve documented the subscribe() and unsubscribe() methods which you will need to retrieve your data.

fetch()

Return a RxJS Observable containing the current snapshot of the query result set as an array.

Note: With both fetch and watch you’ll need to pass your handlers to the subscribe method of the RxJS Observable in order to recieve your data.

const messages = this.db.collection('messages');
messages.fetch()
var messages = $ionicDB.collection('messages');
messages.fetch()

Unlike watch, the fetch command does not update in real time, but rather returns a “snapshot” of the result set as it exists when fetch is executed. The fetch or watch command ends an Ionic query.

const messages = this.db.collection('messages');

messages.fetch().subscribe(
    result => console.log('Result:', result),
    err => console.error(err),
    () => console.log('Results fetched')
);
var messages = $ionicDB.collection('messages');

messages.fetch().subscribe(
    function(result) { console.log('Result:', result)},
    function(err) { console.error(err)},
    function() { console.log('Results fetched')}
);

watch()

Convert a query into a real-time stream. This returns a RxJS Observable which emits the current query result set and continues to emit the new query result set whenever it changes.

Note: With both fetch and watch you’ll need to pass your handlers to the subscribe method of the RxJS Observable in order to recieve your data. To stop receiving emissions from the watch observable you can call unsubscribe().

// get a handle to the channels table in a multi channel chat application
const channels = this.db.collection("channels");

// receive all active channels, listing them every time a channel is
// added, deleted or changed
channels.watch().subscribe(allChannels => {
    console.log('Channels: ', allChannels);
});

// get a handle to the channels table in a multi channel chat application
var channels = $ionicDB.collection("channels");

// receive all active channels, listing them every time a channel is
// added, deleted or changed
channels.watch().subscribe( function(allChannels) {
    console.log('Channels: ', allChannels);
});

This will return output such as this:

Channels: []
Channels: [{id: "50cbeed5-f61e-4539-b6f4-add724f814c2", name: 'general'}]
Channels: [{id: "50cbeed5-f61e-4539-b6f4-add724f814c2", name: 'general'}, {id: "2ab3c5e3-ae71-41c0-8e0b-e954df78e53c", name: 'ionicdb'}]

find()

Retrieve a single document from a Collection.

find(id | object)

The find method may be called with either a key-value pair to match against (e.g., {name: "agatha"} or an id value to look up.

const messages = this.db.collection("messages");

// get the first message from Bob
messages.find({from: "bob"}).fetch();

// get the message with ID "unique_id"
messages.find({id: "unique_id"}).fetch();

// because we are fetching by the document ID, we can use a shorthand
messages.find("unique_id").fetch();
var messages = $ionicDB.collection("messages");

// get the first message from Bob
messages.find({from: "bob"}).fetch();

// get the message with ID "unique_id"
messages.find({id: "unique_id"}).fetch();

// because we are fetching by the document ID, we can use a shorthand
messages.find("unique_id").fetch();

If no matching document exists, no result will be returned and no error will thrown. To explicitly check for this condition, use RxJS’s defaultIfEmpty operator.

const messages = this.db.collection('messages');
messages.find(id).fetch().defaultIfEmpty().subscribe(
    (msg) => {
        if (msg == null) {
            console.log('Message not found');
            return;
        }
    }
);
var messages = $ionicDB.collection('messages');
messages.find(id).fetch().defaultIfEmpty().subscribe(
    function(msg) {
        if (msg == null) {
            console.log('Message not found');
            return;
        }
    }
);

findAll()

Retrieve multiple documents from a Collection.

findAll(id | object[, id | object])

The findAll method can be called with one or more key-value pairs to match against (e.g., {email: "[email protected]"}. Every document that matches the pairs will be returned in a list. (If no documents match, an empty list, [], will be returned.)

const messages = this.db.collection("messages");

// get all messages from Bob, Agatha and Dave
var messageList = messages.findAll({from: "bob"}, {from: "agatha"}, {from: "dave"}).fetch();

// get all messages from Jane and all messages with a high priority
var messageList = messages.findAll({from: "jane"}, {priority: "high"}).fetch();
var messages = $ionicDB.collection("messages");

// get all messages from Bob, Agatha and Dave
var messageList = messages.findAll({from: "bob"}, {from: "agatha"}, {from: "dave"}).fetch();

// get all messages from Jane and all messages with a high priority
var messageList = messages.findAll({from: "jane"}, {priority: "high"}).fetch();

order()

Sort the results of the query by the values of a given field.

order(field[, direction])

Fields passed to order may contain numbers, strings, or even arrays or objects; non-numeric values will be sorted lexicographically, and strings are sorted by UTF-8 codepoint.

The optional second argument must be a string indication sort direction, either "ascending" or "descending". The default is "ascending".

const messages = this.db.collection("messages");

// get all messages, ordered ascending by ID value
messages.order("id", "ascending").fetch();

// "ascending" is the default, so it can be left out
messages.order("id");

// get all messages ordered by time, most recent first
messages.order("time", "descending").fetch();
var messages = $ionicDB.collection("messages");

// get all messages, ordered ascending by ID value
messages.order("id", "ascending").fetch();

// "ascending" is the default, so it can be left out
messages.order("id");

// get all messages ordered by time, most recent first
messages.order("time", "descending").fetch();

limit()

Limit the results of the query to a maximum number of returned documents.

limit(integer)

The limit command takes a single argument, an integer representing the number of items to limit the results to.

const users = this.db.collection("users");

// get the 10 most prolific posters
users.order("postCount", "descending").limit(10).fetch();
var users = $ionicDB.collection("users");

// get the 10 most prolific posters
users.order("postCount", "descending").limit(10).fetch();

above()

Restrict the range of results returned to values that sort above a given value.

above(id | object[, "closed" | "open"])

The above method may be called with either a key-value pair to match against (e.g., {name: "agatha"} or an id value to look up. The second optional parameter must be the string "closed" or "open", indicating that the specified value will be included (closed) or excluded (open) from the result set. The default is excluded (open): above({count: 10}) will return documents with count values above (but do not equal to) 10.

Values in key-value pairs may be numbers, strings, or even arrays or objects; non-numeric values will be sorted lexicographically, and strings are sorted by UTF-8 codepoint.

The above method is often used in conjunction with order, but it may appear after any read operation with the exception of find and limit. (However, limit may appear after above.)

const messages = this.db.collection("messages");

// get all messages with an likes over 100, sorted
messages.order("id").above({likes: 100}).fetch();

// get all messages with a count between 101 and 200, sorted
messages.order("count").below({count: 200}).above({count: 100}).fetch();

// get all users with a reputation score of 50 or over, unsorted
users.above({reputation: 50}, "closed").fetch();
$ionicDB.connect();
const messages = $ionicDB.collection("messages");

// get all messages with an likes over 100, sorted
messages.order("id").above({likes: 100}).fetch();

// get all messages with a count between 101 and 200, sorted
messages.order("count").below({count: 200}).above({count: 100}).fetch();

// get all users with a reputation score of 50 or over, unsorted
users.above({reputation: 50}, "closed").fetch();

below()

Restrict the range of results returned to values that sort below a given value.

below(id | object[, "closed" | "open"])

The below method may be called with either a key-value pair to match against (e.g., {name: "agatha"} or an id value to look up. The second optional parameter must be the string "closed" or "open", indicating that the specified value will be included (closed) or excluded (open) from the result set. The default is excluded (open): below({count: 10}) will return documents with count values lower than (but not equal to) 10.

Values in key-value pairs may be numbers, strings, or even arrays or objects; non-numeric values will be sorted lexicographically, and strings are sorted by UTF-8 codepoint.

The below method may only be used after order, although other methods may be used after it.

const messages = this.db.collection("messages");

// get all messages with an likes below 100, sorted
messages.order("id").below({likes: 100}).fetch();

// get all messages with an count between 101 and 200, sorted
messages.order("count").below({count: 200}).above({count: 100}).fetch();

// get all users with a reputation score of 50 or below, sorted
users.order("reputation").below({reputation: 50}, "closed").fetch();
var messages = $ionicDB.collection("messages");

// get all messages with an likes below 100, sorted
messages.order("id").below({likes: 100}).fetch();

// get all messages with an count between 101 and 200, sorted
messages.order("count").below({count: 200}).above({count: 100}).fetch();

// get all users with a reputation score of 50 or below, sorted
users.order("reputation").below({reputation: 50}, "closed").fetch();

currentUser()

Returns a query for the current user, that you can run by calling either watch() or fetch(). This method is a shortcut for:

this.db.collection('users').find(this.user.id)
$ionicDB.collection('users').find($ionicUser.id)

The query result is a user object if authentication is enabled. This method raises an error if authentication is not enabled.

$ionicDB.currentUser().watch().subscribe( function(user) { console.log(user)});
this.db.currentUser().watch().subscribe( (user) => console.log(user) );

Aggregates and models

Queries on multiple Collections can be combined using the aggregate() method, and aggregates can be turned into parameterized templates using the model() method.

aggregate()

Combine the results of multiple db queries into one result set.

aggregate( object | array )

var userName = "ionitron";
$ionicDB.aggregate({
    name: userName,
    user: $ionicDB.collection('users').find(userName),
    activity: {
        posts: $ionicDB.collection('posts').findAll({user: userName}),
        topComments: $ionicDB.collection('comments').findAll({user: userName}).order('rating', 'descending').limit(10)
    }
}).watch().subscribe(subscribeFunction);
const userName = "ionitron";
this.db.aggregate({
    name: userName,
    user: this.db.collection('users').find(userName),
    activity: {
        posts: this.db.collection('posts').findAll({user: userName}),
        topComments: this.db.collection('comments').findAll({user: userName}).order('rating', 'descending').limit(10)
    }
}).watch().subscribe(subscribeFunction);

The values for fields in aggregates may contain:

Observables inside an aggregate are called when the aggregate is subscribed to, and behave identically whether the aggregate is called with fetch() or watch(). So an aggregate such as the following:

this.db.aggregate({
    counter: Observable.timer(0, 1000),
    result: this.db.collection('foo').find('bar')
}).watch().subscribe({ next(x) { console.log(x) }});
$ionicDB.aggregate({
    counter: Observable.timer(0, 1000),
    result: $ionicDB.collection('foo').find('bar')
}).watch().subscribe({ next(x) { console.log(x) }});

will be emitted every time the document in the result query is changed and every time the counter is incremented by the timer.

Arrays are not flattened. So an aggregate with a field such as:

this.db.aggregate({
...
dogShow: [this.db.collection('owners'), this.db.collection('pets')],
...
}
$ionicDB.aggregate({
...
dogShow: [$ionicDB.collection('owners'), $ionicDB.collection('pets')],
...
}

will result in output similar to:

{
...
dogShow: [[{name: 'bob'}, {name: 'agatha'}, ...], [{name: 'fluffy'}, {name: 'fido'}, ...]]
...
}

Aggregates can be nested, although this is equivalent to simply using objects for the “inner” aggregates.

/// this...
this.db.aggregate({
    foo: this.db.aggregate({ ... })
});

// ...is equivalent to this
this.db.aggregate({
    foo: { ... }
});
/// this...
$ionicDB.aggregate({
    foo: $ionicDB.aggregate({ ... })
});

// ...is equivalent to this
$ionicDB.aggregate({
    foo: { ... }
});

model()

Create a template for aggregates, using parameters.

model( function )

The aggregate example could be rewritten with model this way:

const userModel = this.db.model((userName) => {
    return {
        name: userName,
        user: this.db.collection('users').find(userName),
        activity: {
            posts: this.db.collection('posts').findAll({user: userName}),
            topComments: this.db.collection('comments').findAll({user: userName}).order('rating', 'descending').limit(10)
        }
    }
});

userModel("ionitron").watch().subscribe(subscribeFunction);
var userModel = $ionicDB.model((userName) => {
    return {
        name: userName,
        user: $ionicDB.collection('users').find(userName),
        activity: {
            posts: $ionicDB.collection('posts').findAll({user: userName}),
            topComments: $ionicDB.collection('comments').findAll({user: userName}).order('rating', 'descending').limit(10)
        }
    }
});

userModel("ionitron").watch().subscribe(subscribeFunction);

You can do anything with model that you can do with aggregate, including nesting models.

Write Operations

Note: All read and write return an RxJS Observable. Observable’s have an extensive API which you can read about here. For convenience we’ve documented the subscribe() and unsubscribe() methods which you will need to retrieve your data.

insert()

Insert one or more new documents into a Collection.

insert(object | list of objects)

The insert method can be called either with an object representing a single document, or a list of objects. The objects must have unique string id values, and must have id values that do not already exist in the collection or an error will be raised. If id is not specified a UUID will be randomly generated.

const messages = this.db.collection("messages");

// Insert a single document. There must not be a document with an id of "my_id" in
// the messages collection.
messages.insert({
    id: "my_id",
    from: "bob",
    text: "Hello from IonicDB"
});

// Insert multiple documents at once. Documents will be assigned  a unique `UUID` for `id`.
messages.insert([
    {
        from: "agatha",
        text: "Meet at Smugglers' Cove on Saturday"
    },
    {
        from: "bob",
        text: "Would Superman lose a fight against Wonder Woman?"
    }
]);
var messages = $ionicDB.collection("messages");

// Insert a single document. There must not be a document with an id of "my_id" in
// the messages collection.
messages.insert({
    id: "my_id",
    from: "bob",
    text: "Hello from IonicDB"
});

// Insert multiple documents at once. Documents will be assigned  a unique `UUID` for `id`.
messages.insert([
    {
        from: "agatha",
        text: "Meet at Smugglers' Cove on Saturday"
    },
    {
        from: "bob",
        text: "Would Superman lose a fight against Wonder Woman?"
    }
]);

replace()

Replace one or more existing documents within a Collection.

replace(object | list of objects)

This is similar to insert, but instead of requiring documents to have new id values, replace requires documents to have existing id values in the Collection. If you try to insert new documents with replace, an error will be raised.

store()

Insert one or more documents into a Collection, replacing existing ones or inserting new ones based on id value.

store(object | list of objects)

The store method is a combination of insert and replace:

update()

Modify one or more existing documents within a Collection.

update(object | list of objects)

The update method can be called either with an object representing a single document, or a list of objects. The objects must have id values that already exist in the collection or an error will be raised. This functions similarly to replace, but the update command will only change the specified fields in documents it affects; unspecified fields will be left untouched.

// Update a single document. This will raise an error if there is not an
// existing document in the messages collection with an id value of "my_id".
messages.update({
    id: "my_id",
    from: "bob",
    text: "Hello"
});

// Update multiple documents at once. All documents must already exist.
// Fields that are not specified in the update command will be left
// unmodified.
messages.update([
    {
        id: "my_id_1",
        text: "Meet at Smugglers' Cove on Saturday"
    },
    {
        id: "my_id_2",
        text: "Would Superman lose in a fight against Wonder Woman?"
    }
]);

upsert()

Update one or more documents in a Collection, modifying existing ones or inserting new ones based on id value.

upsert(object | list of objects)

The upsert method is a combination of insert and update:

remove()

Delete a single document from a Collection.

remove(id | object)

The remove method may be called with either an object to be deleted or an id value. In the object case, the object must include an id key.

const messages = this.db.collection("messages");

// get the first message with the text "delete me"
let messageObject;
messages.find({text: "delete me").fetch().subscribe(
    (result) => { messageObject = result; }
);

// delete that object
messages.remove(messageObject);

// it may also be deleted by passing an object just with an ID key if ID is known
messages.remove({id: "custom_id"});

// because we are deleting based on the document ID, we can use a shorthand
messages.remove("custom_id");
var messages = $ionicDB.collection("messages");

// get the first message with the text "delete me"
var messageObject;
messages.find({text: "delete me"}).fetch().subscribe(
    function(result) { messageObject = result; }
);

// delete that object
messages.remove(messageObject);

// it may also be deleted by passing an object just with an ID key if ID is known
messages.remove({id: "custom_id"});

// because we are deleting based on the document ID, we can use a shorthand
messages.remove("custom_id");

removeAll()

Delete multiple documents from a Collection.

removeAll([id, id, ...] | [object, object, ...])

The removeAll method must be called with an array of objects to be deleted, or id values to remove. The objects must have id keys. You can mix id values and objects within the array.

const messages = this.db.collection("messages");

// delete messages with the IDs "good_id", "bad_id", and "uGlY_Id"
messages.removeAll(["good_id", "bad_id", "uGlY_Id"]);

// find and delete all messages from Bob and Agatha
// this example uses RxJS's "mergeMap" command to execute the removeAll and return
// another observable to pass to subscribe for reporting purposes
messages.findAll({from: 'bob'}, {from: 'agatha'}).fetch()
    .mergeMap(messageList => messages.removeAll(messageList))
    .subscribe({
        next(id)   { console.log(`id ${id} was removed`) },
        error(err) { console.error(`Error: ${err}`) },
        complete() { console.log('All items removed successfully') }
    });
var messages = $ionicDB.collection("messages");

// delete messages with the IDs "good_id", "bad_id", and "uGlY_Id"
messages.removeAll(["good_id", "bad_id", "uGlY_Id"]);

// find and delete all messages from Bob and Agatha
// this example uses RxJS's "mergeMap" command to execute the removeAll and return
// another observable to pass to subscribe for reporting purposes
messages.findAll({from: 'bob'}, {from: 'agatha'}).fetch()
    .mergeMap(messageList => messages.removeAll(messageList))
    .subscribe({
        next(id)   { console.log(`id ${id} was removed`) },
        error(err) { console.error(`Error: ${err}`) },
        complete() { console.log('All items removed successfully') }
    });

Authentication

IonicDB integrates with the existing Auth service. When you create your database Auth is disabled by default so that you can immediately connect to your database and start playing around.

In order to enable Auth you must:

Configuring Authentication

Note: Once you’ve enabled authentication you must first the login with Auth before you attempt to connect or use the read or write API’s.

Enable Authentication in the Dashboard

First make sure Authentication is enabled in the dash.

Enable Authentication in the Cloud Client

You will need to enable Authentication in the client as well.

Add a database object to your CloudSettings in the src/app/app.module.ts file.

import { CloudSettings, CloudModule } from '@ionic/cloud-angular';

const cloudSettings: CloudSettings = {
  'core': {
    'app_id': 'APP_ID'
  },
  'database': {
    'authType': 'authenticated'
  }
};

@NgModule({
  declarations: [ ... ],
  imports: [
    IonicModule.forRoot(MyApp),
    CloudModule.forRoot(cloudSettings)
  ],
  bootstrap: [IonicApp],
  entryComponents: [ ... ],
  providers: [ ... ]
})
export class AppModule {}

Add an extra database object to the $ionicCloudProvider.init() funtion in the www/js/app.js file.

angular.module('starter', ['ionic', 'ionic.cloud', 'starter.controllers', 'starter.services'])

.config(function($ionicCloudProvider) {
  $ionicCloudProvider.init({
    "core": {
      "app_id": "APP_ID"
    },
    "database": {
      "authType": "authenticated"
    }
  });
})

.run(function($ionicPlatform) {
  ...
})

See configuring the Ionic Services Client for more detail about this step.

Login Before Connecting

Now you will need to login using Auth before you can connect.

.controller(function($ionicDB, $ionicAuth) {
  var details = {email: "[email protected]", password: "puppies123"};

  if($ionicAuth.isAuthenticated()){
    // Already logged in safe to connect
    $ionicDB.connect();
  } else {
    // Need to login
    $ionicAuth.login('basic', details).then( function() {
      // Now it is safe to connect
      $ionicDB.connect();
    });
  }
})
import { Component } from '@angular/core';
import { Auth, Database } from '@ionic/cloud-angular';

@Component({
  ...
})
export class LoginPage {

  constructor(public auth: Auth, public db: Database) {
    var details = {email: "[email protected]", password: "puppies123"};

    if(this.auth.isAuthenticated()){
      // Already logged in safe to connect
      this.db.connect();
    } else {
      // Need to login
      this.auth.login('basic', details).then( () => {
        // Now it is safe to connect
        this.db.connect();
      });
    }
  })
}

Permissions

Permissions allow you to restrict what data you users can access and store.

Note: By default permissions are turned off.

The Whitelist

The permission system is based on a whitelist. When permissions are enabled any operation on an collection is disallowed by default, unless there is a permission that allows the operation.

A permission has two properties that define which operations it covers:

Groups

There are currently you two special groups you can use to add some basic database permissions for your users.

Note: Custom groups will be available soon and will allow you to create and manage groups of users and their permissions with more granularity.

Configuring Permissions

Arbitrary numbers of permissions can be defined on any of the groups created through the Ionic Dashboard. Doing so is simple, first navigate to the Permissions section of the Database tab, then select the group you wish to edit permissions for from the dropdown.

At the most basic level, a permission has 4 parts:

The type of template is selected using the dropdown menu on the permission creation form. There are two predefined templates you can use or you can create a custom template if more granularity is needed.

Predefined Templates

There are two predefined templates: read and write, allowing all cobinations of read or write operations on the target collection, respectively.

For example, selecting read on a rule for the collection chats would allow all members of the group to perform any read operations on the chats collection.

For more complicated access control to collections, a custom template is needed.

Custom Templates

Selecting custom from the template type dropdown will show an input box for a custom permission. Here it’s possible to write templates which allow actions to a group at a much more granular level.

Custom templates allow you to limit the type and target of all operations on the devices of group members. They follow the same syntax as the client-side read and write API, with a few helper functions to increase functionality.

A simple custom template could look like:

name: find_announcements
collection: messages
template: findAll({type: "announcement"})

The above permission will allow any user in the group to call the following in the cloud client:

this.db.collection('messages').findAll({type: "announcement"})
$ionicDB.collection('messages').findAll({type: "announcement"})

Templates can be made more specific thus being more restrictive. If a template is terminated with fetch() or watch() only that type of query is allowed.

Here are two examples of custom templates for a chats collection and the list of client-side operations they allow:

Template 1
name: find_public
collection: chats
template: findAll({type: "public"})
// Allowed operations in client
this.db.collection("chats").findAll({type: "public"}).watch()
this.db.collection("chats").findAll({type: "public"}).fetch()
this.db.collection("chats").findAll({type: "public"}).limit(100).fetch()
this.db.collection("chats").findAll({type: "public"}).order("created", "descending").limit(100).watch()
this.db.collection("chats").findAll({type: "public"}) //followed by any syntax legal operation 
// Allowed operations in client
$ionicDB.collection("chats").findAll({type: "public"}).watch()
$ionicDB.collection("chats").findAll({type: "public"}).fetch()
$ionicDB.collection("chats").findAll({type: "public"}).limit(100).fetch()
$ionicDB.collection("chats").findAll({type: "public"}).order("created", "descending").limit(100).watch()
$ionicDB.collection("chats").findAll({type: "public"}) //followed by any syntax legal operation 
Template 2
name: find_public
collection: chats
template: findAll({type: "public"}).limit(100).fetch()
// Allowed operations in client
this.db.collection("chats").findAll({type: "public"}).limit(100).fetch()
// No other calls are allowed since the template is specific
// Allowed operations in client
$ionicDB.collection("chats").findAll({type: "public"}).limit(100).fetch()
// No other calls are allowed since the template is specific

Placeholders

There are two special placeholder functions you can use in a custom template, that will help to create more robust permissions.

The any() placeholder matches any value.

// Allow users to look up messages from any user
.findAll({owner: any()})

You can also specify a list of legal values by passing them to the any() call:

// Allow users to look up messages of type "shared" or "announcement"
.findAll({type: any('shared', 'announcement')})

The userId() placeholder matches the ID of the currently authenticated user. If no user is currently authenticated, it will match the null value. Authentication must be enabled for user to have a user ID associated with them.

// Allow users to read their own messages
.findAll({owner: userId()})

For documentaion on how to use Authentication with IonicDB and the group and permissions system go here.

Observables

RxJS Observables are used extensively in the IonicDB client. They present an expansive API which you can read about here. For your convenience we’ve documented the two methods you’ll need most commonly.

subscribe()

Subscribe is a method on an Observable. It takes a up to three functions as parameters.

When subscribe is chained off a read function (i.e., fetch) it takes three callback functions:

this.db.connect();
const messages = this.db.collection('messages');

messages.fetch().subscribe(
    result => console.log('Result:', result),
    err => console.error(err),
    () => console.log('Results fetched')
);
$ionicDB.connect(); 
var messages = $ionicDB.collection('messages');

messages.fetch().subscribe(
    function(result) { console.log('Result:', result)},
    function(err) { console.error(err)},
    function() { console.log('Results fetched')}
);

Note that when subscribe is chained after fetch, the next callback will receive a single array containing the entire result set at once and then the observable will complete. When subscribe is chained after watch the next function will continue to emit as the documents in the query change and the complete function will not execute unless there is an error. User unsubscribe() to stop receiving emissions from a watch observable.

var messages = this.db.collection('messages');
messages.fetch().subscribe(readFunction, errorFunction, completedFunction)
messages.store().subscribe(writeFunction, errorFunction)
messages.watch().subscribe(changefeedFunction, errorFunction)
var messages = $ionicDB.collection('messages');
messages.fetch().subscribe(readFunction, errorFunction, completedFunction)
messages.store().subscribe(writeFunction, errorFunction)
messages.watch().subscribe(changefeedFunction, errorFunction)

When chained off a write function (e.g., store, upsert), it takes two callback functions:

const messages = this.db.collection('messages');
messages.store([
    {
        from: "agatha",
        text: "Meet at Smugglers' Cove on Saturday"
    },
    {
        from: "bob",
        text: "Would Superman lose a fight against Wonder Woman?"
    }
]).subscribe(
    (id) => console.log("id value:", id),
    (err) => console.error(err)
);
var messages = $ionicDB.collection('messages');
 messages.store([
    {
        from: "agatha",
        text: "Meet at Smugglers' Cove on Saturday"
    },
    {
        from: "bob",
        text: "Would Superman lose a fight against Wonder Woman?"
    }
]).subscribe(
    function(id) { console.log("id value:", id)},
    function(err) { console.error(err)}
);

This would produce output similar to:

id value: f8dd67dc-2301-487a-85ab-c4b573acad2d
id value: 2ab3c5e3-ae71-41c0-8e0b-e954df78e53c

unsubscribe()

Unsubscribe can be used on the subscription returned from the subscribe method in order to stop receiving emissions.

let subscription = this.db.collection('messages').watch().subscribe(
    result => { console.log(result); },
    err => { console.error(err); }
);

// Unsubscribe from receiving more messages
subscription.unsubscribe();
var subscription = $ionicDB.collection('messages').watch().subscribe(
    result => { console.log(result); },
    err => { console.error(err); }
);

// Unsubscribe from receiving more messages
subscription.unsubscribe();

FAQ

The examples below show some common actions you may want in an app utilizing IonicDB.

Enforcing Schema

Schema in IonicDB is enforced through the permissions system. The following shows how to enforce that a document contains only the specified fields.

Note: Currently it is not possible to enforce that a field is of a particular type.

To enforce that any documents stored into our chats collection have a text and a created field as well as type field that is either public or private make the following custom template in the Permissions section of the dashboard.

permission name: chat_document
collection name: chats
type: custom
store({text: any(), created: any(), type: any('public', 'private')})

Enforcing Ownership

In order to enforce ownership of documents authentication must be enabled.

First, we need to add a custom template for the authenticated group on the chats collection. It will mandate as follows:

Giving us the following rule:

insert({owner: userId(), message: any()})

To restrict read access to only messages owned by the current user we would make another template:

findAll({owner: userId()})

Client-side we need to store the correct info in the app when new chats are created, like so:

import { Component } from '@angular/core';
import { Database, User } from '@ionic/cloud-angular';

@Component({
  ...
})
export class AboutPage {

  public activeMessage: string;

  constructor(public db: Database, public user: User) {}

  sendMessage() {
    this.db.collection('chats').store({message: this.activeMessage, owner: this.user.id});
  }
}
.controller('ChatCtrl', function($scope, $ionicDB, $ionicUser) {
  $scope.activeMessage = '';

  $scope.sendMessage = function() {
    $ionicDB.collection('chats').store({message: $scope.activeMessage, owner: $ionicUser.id});
  }
}

And messages can be retrieved with:

import { Component } from '@angular/core';
import { Database, User } from '@ionic/cloud-angular';

@Component({
  ...
})
export class AboutPage {

  public myMessages: string;

  constructor(public db: Database, public user: User) {
    this.db.collection('chats').findAll({owner: this.user.id})
    .subscribe( msgs => {
      this.myMessages = msgs;
    }
  }
}
.controller('ChatCtrl', function($scope, $ionicDB, $ionicUser) {
  $ionicDB.collection('chats').findAll({owner: $ionicUser.id})
  .subscribe(function(msgs) {
    $scope.myMessages = msgs;
  });
}

Using IonicDB in Node.js

The first thing you will want to do is to generate an API Token that you can use to authenticate your Node app with. You can find out how here.

Using an API Token to connect to connect to IonicDB will enable Admin privilege. This means that anything connecting with an API Token has full read and write privileges on all collections. Permissions will NOT be enforced.

Make sure that only trusted sources that you have complete control over connect with an API Token.

Along with your other npm dependencies in you package.json you will need both the @ionic/db and ws packages. Here is a sample package.json and the index.js for a simple Express server using the IonicDB Client.

{
  "name": "IonicDB-Node",
  "version": "0.0.1",
  "description": "A demo of using the IonicDB Client in a Node environment",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "IonicDB",
    "Node"
  ],
  "author": "Nick Hyatt",
  "license": "ISC",
  "dependencies": {
    "@ionic/db": "^0.1.0",
    "express": "^4.14.0",
    "ws": "^1.1.1"
  }
}

Notice in the code below that you need to pass in the ws library as the WebSockeCtor property in the IonicDB settings since the Node environment doesn’t have a built in WebSocket Library.

var WebSocket = require('ws');
var express = require('express');
var IonicDB = require('@ionic/db').IonicDB

var app = express();

var api_token = "YOUR_API_TOKEN";
// The more secure way would be to pass in your API_TOKEN
// through the environment and start your app with using:
// API_TOKEN=TOKEN node index.js
//var api_token = process.env.API_TOKEN

var db_settings = {
  app_id: "YOUR_APP_ID",
  authType: "authenticated",
  WebSocketCtor: WebSocket
}

var db = new IonicDB(db_settings);
db.setToken(api_token);
db.onConnected().subscribe(function(){
  console.log("Connected to IonicDB!");
})

db.connect();

app.get('/documents', function(req, res) {
   // This is a simple example which wouldn't actually require
   // an Express app however you can imagine doing more complex
   // things here such as hitting existing API's elsewhere,
   // doing more complex calculations, etc.
   db.collection('documents').fetch().subscribe(function(docs){
      res.send(docs);
   });
})

app.listen(8900, function () {
  console.log('Express app listening on port 8900!')
})

Copying the above files and replacing YOUR_APP_ID and YOUR_API_TOKEN should be enough to get you connected from your Node app.

Services

    API

      General