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:
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:
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).
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:
As we can see, these Property relations point to Property instances. Which have a model and a type:
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.
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:
In the end the overall, simplified data model for our example looks like this:
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:
When queried, this will return a sub-graph that will look like something like this:
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.