Introduction to the Data Cloud

Olympe's Data Cloud layer extends the Data Flow concepts to the cloud. That is, it makes it possible to share data across the network and easily create fully distributed, real-time applications.

Graph Database

To that effect Olympe organises the data as a graph. At his core the DataCloud is a distributed graph database. A graph database, as the name implies, is a database that uses graph structures for semantic queries with nodes, relations, and properties to represent and store data.

Let's take a look at an example:

GraphDB

Here we have 2 persons, John & Alice, who work at the same company, Acme. We also know that John owns a car, a Ford to be precise. There are nodes representing 3 different types of data: Person, Company & Car. Each nodes of these types can have properties like Name, Age or Brand. These nodes are linked by relations like 'WorkAt', 'Owns' and 'Knows'. These relations can also have properties, like 'Since' or 'Salary'.

Note that all these nodes & relations have a unique Id as well. Representing data in the form of a graph has a number of advantages specially when it comes to querying complex, interlinked data sets. In particular, finding and following relationships, like 'WorkAt' or 'Owns', is fast and efficient compared to traditional, table based databases.

Olympe Data Model

Olympe's data model has to cover both the data and its definition. As an example it has to store a person's information (first name, last name, age) but also that a person's data, indeed is composed of a first name, a last name and an age. That's the difference between an instance ( John Smith, 32) and its model (Person). This is similar to the Instance & Class relationship of Object Oriented languages like Java. In Olympe, every object has to have an associated model. So, before we can store data in the DataCloud, we need to define the various models we want to use. Back to our example, let's say we want to add 2 Persons to the DataCloud, John Doe and Alice Smith, We need to define the model Person. And the link between the instances and their model is achieved with a Model relation:

DM1

Since a model is also an Object in our graph, it has to have a model. That model is 'Object' which is a special instance in our graph (again very similar to Java).

DM2

Now, that model is supposed to describe what makes a Person. In our example, it means that it has 3 properties: First Name, Last Name & Age. Again this is done by using a relation, the Property relation this time:

DM3

As we can see, these Property relations point to Property instances. Which have a model and a type:

DM4

Here we can see that First Name & Last Name are of the String type, while Age is of the Number type. And of course the model is Property. But how do we know that an instance of Property has to have a Type relationship? That's the job of the model. This is done with 2 special relations: Origin & Destination. Origin links a model to a Relation while Destination links a Relation to an Object.

DM5

This diagram tells us that a Property must have a Type relation to another Object. Note that a Relation instance contains other information, like cardinality, but for the sake of simplicity we're going to ignore these for now. Similarly this is how it is specified that an Object can have properties. The 'Object' root object has an 'Origin' & 'Destination' relation with Property:

DM6

In the end the overall, simplified data model for our example looks like this:

DM7

It may look a bit complicated, but there are only 3 important concepts to remember:

  • Model: Every object has a model.
  • Relation: Defined by the model, they link objects together.
  • Property: Defined by the model, represents an attribute of an Object.

olympe.dc.Sync

Sync is the base class for the DC. It represents a reference to an object in the cloud. A Sync uses the unique ID, or tag in Olympe parlance, to identify its associated object. As for most DC operations, a Sync is created via the getInstance static method.

Example:

const s = olympe.dc.Sync.getInstance(‘016b55ed385836c051ce’);
const u = olympe.dc.Sync.getInstance(‘016b55ed385836c051cf’);

The second example specifies that this particular instance is of a specific type, or sub-class of Sync. Once you have an instance of Sync, you can access its properties by calling the getPropertyAs??? methods (1 method per type):

  • getPropertyAsString
  • getPropertyAsNumber
  • getPropertyAsBoolean
  • getPropertyAsColor
  • getPropertyAsDateTime

You need to know the tag of that property since it's the only argument. So if we continue with our previous example:

const u = olympe.dc.Sync.getInstance(‘016b55ed385836c051cf’);
const login = u.getPropertyAsString(olympe.dm.User.loginProp);

Note: login, in this example, is a data flow. Which means that its value will change automatically if the entry for that User is updated, wherever it resides in the data cloud.

Sub-classes of Sync usually provide easy access to these properties with convenience methods. Quite a few of these sub-classes are in the olympe.dm namespace. For instance, when retrieving a User, like in the previous example we can use specific methods instead:

const u = olympe.dc.Sync.getInstance(‘016b55ed385836c051cf’);
const login = u.getLogin();
const desc = u.getDescription();

We did specify the expected type, olympe.dm.User here, in getSync which allows us to use the methods of that sub-class of Sync. You will have to admit this is more user-friendly and a lot easier to read.

Finally, if the type is not one of the base ones, then the property will be accessed as a Sync: getPropertyAsSync. So, getting access to properties is rather straightforward, but what about following relations? By that I mean accessing instances connected to another instance via a specific relation? There are 2 cases: Either the relation you want to follow points to a single instance (this would be the case for the Model relation for instance, since an instance can have only model) or potentially multiple instances (isFriendWith is a good example of this, since one person can have multiple friends).

In the first case, you would follow that relation by using getFirsRelated.

const friend = person.getFirstRelated(isFriendWith, Person);

This gives you a Proxy of a sub-class of Sync (Person in this case). Now if you want to access all the friends of that person, you would call the getRelated method, which returns, this time, a list of Person, or more precisely, a ListDef.

olympe.dc.ModelInfo

Since the difference between a pure instance and a model is important, the Olympe platform provides an API to easily access all the information of a given model:

  • Name
  • Properties and inherited properties (from parent models)
  • Relations and inherited relations (from parent models)

This is accessible through the ModelInfo class.

Example:

const modelInfo = new olympe.dc.ModelInfo(objectSync.getModel());
const props = modelInfo.getProperties();
const rels = modelInfo.getRelations();

olympe.dc.ListDef

A ListDef represents the result of database query applied to the data cloud. It encapsulate an ordered list of instances that match the query condition. Sync.getRelated is such a query. It means 'Get all instances connected to this particular instance via this particular relation'. Here again, the result will be kept 'live' by the DC manager, so if relations are added, or removed, the list will be updated accordingly. That's why a ListDef is usually handled by specifying 2 callbacks, one for additions and one for removals:

const friends = person.getRelated(isFriendWith, Person);
friends.forEach(
    (friend, k) => {
        // Friend added to the list
    },
    k => {
        // Friend removed from the list
    }
);

In this example the first callback is invoked every time a new 'Friend' relation is created, while the second one is invoked every time such a relation is deleted. Note that if the list is not empty (relations already exists) then the first callback is immediately invoked for all items on the list.

If you want to walk through a snapshot of the list you can use the forEachCurrentValue method:

const friends = person.getRelated(isFiriendWith, Person);
friends.forEachCurrentValue(
    (friend, k) => {
        // Friend currently in the list
    }
);

This will iterate through all the instances of Person connected at execution time via the isFriendWith relation to person. This execution happens only once.

Transformers

When dealing with lists the need for sorting or filtering comes very quickly. This is where transformers come in. A Transformer is applied to a ListDef to create a new, different ListDef based on the criteria of that transformers. olympe.dc.Transformer is an interface, but some commonly used ones, like Sort and Filter, are defined in olympe.dc.transformers.*. Other transformers include following relations or merging two ListDefs.

Example:

const friends = person.getRelated(isFriendWith, Person);
const friendsOfFriends = friends.transform(new olympe.dc.transformers.Related(isFriendWith),
                                           new olympe.dc.transformers.Distinct());

In this example we apply 2 Transformers to the friends list in order to get the list of "friends of friends" by following the isFriendWith relation to all the friends. The 2nd transformer, Distinct ensure we get rid of duplicates which are likely to happen.

Predicates

Some transformers, like Filter, require extra information to function. For instance, let's say we want to filter our friend list to only include friends who owns a dog. We need to be able to tell the Filter transformers how to figure that out. This is what Predicates are for. Predicate is an interface for implementing function that will return true or false based on the instance. Some predefined predicates reside in olympe.dc.predicates, like HasRelated which tests whether an instance has specific relations connected to it. There are also some predicates that combines other predicates, like And and Or.

Example:

// Return all the transitions going out the specified screen
const lst = this.getChildren().transform(
    new olympe.dc.transformers.Filter(
        new olympe.dc.predicates.And(
            new olympe.dc.predicates.InstanceOf(olympe.sc.ui.ScreenTransition.entry.getTag()),
            new olympe.dc.predicates.HasRelated(olympe.sc.ui.ScreenTransition.fromRel, olympe.dc.RelationDirection.RIGHT, tag)
        )
    )
);

Comparators

Comparators are similar to predicates but are used by transformers that need to compare two instances. Sort, in particular, uses a comparator to determine the order of the instances. Similarly Comparator is an interface but some commonly used implementations, like Number can be found in olympe.dc.comparators.

ValueDef

Most Comparators require a way to access the value to compare from the instance. Very often, this would be one of the properties of that instance, however this is not always that simple. For instance, when sorting a list of persons, you would probably sort it alphabetically based on the last name & first name. 'Smith, john' should appear before 'Smith, Wallace' for instance. How to determine what to actually compare is done through ValueDefs. Again ValueDef is an interface and several pre-defined, commonly used implementations can be found in olympe.dc.valuedefs.

Example:

const friends = person.getRelated(isFriendWith, Person);
const sorted = friends.transform(
    new olympe.dc.transformers.Sort(
        new olympe.dc.comparators.String(
            new olympe.dc.valuedefs.StringProperty(lastNameProp)
        )
    )
);

In this particular case, we are sorting the friends list based on the last name property (lastNameProp being the tag for that particular property).

ListDefBuilder

While it is perfectly fine to manipulate ListDefs and Transforms directly, the ListDefBuilder helper class provides an easier to use, and more readable, API. You create a ListDefBuilder by providing the base tag and the type of the list values, then use the various methods to combine the transforms you need. Once done, you generate the actual ListDef by calling build().

Example:

const builder = new olympe.dc.ListDefBuilder(manager, tag, olympe.dm.String)
    .​follow(typeRel)
    .contains(
        new olympe.dc.valuedefs.StringProperty(prop1),
        new olympe.dc.valuedefs.Constant('pro')
    );
const l = builder.build();
l.forEach(
    (value, key) => { /** on add... */ },
    (key) => { /** on remove... */ }
);

Transactions

While ListDefs are used to query the datacloud, Transactions are used to update it. This is where you create, update or delete instances and relations. A transaction is a series of operations (create, update or delete) that will be executed. If an error occurs in any of the operations then the whole transaction is rolled-back. In essence, from the database point of view, it's an atomic operation: either everything is applied successfully or none of it is. The sequence is somewhat similar to builders: You create a Transaction, add the operations then execute it.

Example:

const trans = new olympe.dc.Transaction();
trans.create(modelTag, instanceTag);
trans.update(instanceTag, propertyTag, 10);
trans.delete(otherInstanceTag);
trans.execute(
    (ok, msg) => {
        if (!ok) {
            console.warn(`transaction failed: ${msg}`);
        }
    }
);

olympe.dc.GraphDef

GraphDefs are an alternative to ListDefs and, partially, to Transactions. They allow the definition of a sub-graph from the data cloud, and the performing of some operations (read, update, delete) on it. The way you describe a sub-graph is by providing a base instance, usually by its tag, then relations to follow.

Example:

new olympe.dc.GraphDef('joe')
    .follow(Friend).follow(Friend)
    .backToRoot()
    .follow(Partner)
    .follow(Friend).follow(Friend)
    .execute((error, result) => {
        // Use result
    });

This GraphDef will match 'Joe', his partner, their friends and their friend's friends. The GraphDef itself has the following structure:

GaphDefTree

When queried, this will return a sub-graph that will look like something like this:

GaphDef

The possible actions on a GraphDef are:

  • Subscribe: Query the DB and create data flows for each instances of the sub-graph
  • Get: Query the DB, but create a snapshot of the value.
  • Delete: Delete instances and relations from the DB.
  • Update: Update properties of instances.

Delete is of particular interest since it allows for pertinent deletion:

new olympe.dc.GraphDef('joe', olympe.dc.GraphDefOperation.DELETE)
    .activateFollowRules(olympe.dc.FollowRules.DELETE)
    .execute((error, result) => {
        // Check for errors
    });

The activateFollowRules() call automatically creates the relations in the GraphDef to be consistent with the data model as specified by its developer. It will, for instance, specifies which relations to delete and which related instances to delete as well.