Tutorial: Writing data

Serverless DatabaseData Structure Writing data

To write data into a Webcom application database, three methods are available:

Method Description
set This method (over)writes data at a data node of the database.
merge
(or update)
This method updates one or more child nodes of a given data node by merging them into this data node while preserving already existing child nodes.
push This method adds a child node to a given data node, with a unique and chronological ID. It is particularly useful to build lists of data (e.g. chat messages within a chat room).

Setting data

The set method writes new data into the data node on which it is applied. If it already contains data, all data at this node (including any child nodes) will be overwritten.

As an example related to an address book application, let's start by saving some contact details. Each contact contains a unique userName, as well as a firstName, a lastName, a phoneNumber, an email and a birthday.

First, we get the root data node of our Webcom application database, where to save our contact details. Our application is called "webcom-addressbook":

var adbk = new Webcom("webcom-addressbook");

On Android environments, the name of the Webcom application is usually defined within the webcom.properties asset file. Then, the first step is to get a DatasyncManager, an instance of which is typically created for each Android activity.

val myApp = WebcomApplication.default // the app defined by the webcom.properties file
val manager = myApp.datasyncService.createManager()

On iOS environments, the name of the Webcom application is commonly set within the Info.plist file.
In addition, we need a "manager", which is typically defined locally to a view controller.

let adbk = Webcom.defaultApplication
let manager = adbk.datasyncService.createManager()

With the REST API, the base URL for write operations is (replace “<your-app>” with your actual application identifier):
https://io.datasync.orange.com/datasync/v2/<your-app>/data
Then it can be completed with the actual path of the targeted data node within the Webcom database.

Then we eventually use the set method to create an object under the "contacts" node.

In our application, this object is intended to represent a contact within our address book and consists in a child node named by the userName of this contact, which in turn has one child node for each attribute of the contact: firstName, lastName, phoneNumber, email and birthday. The object representing the contact must actually be serializable to a JSON object whose properties are key-value pairs for all the attributes listed above.

The set() method accepts strings, numbers, booleans, null, arrays or JSON objects. Passing null actually removes the data at the node on which it is called. In our example we simply pass the JSON object representing our contact details:

const maccaNode = adbk.child("contacts/macca"); // get a reference on the data node representing the "macca" contact
const macca = { // our JSON object representing the contact details for "macca"
    birthday: "June 18, 1942",
    firstName: "Paul",
    lastName: "McCartney",
    phoneNumber: "020 1234 6541",
    email: "paulo@apple.com"
};
maccaNode.set(macca);

The set() method accepts primitive types such as strings, numbers, booleans, lists and maps. In addition, and more interestingly, it also accepts serializable classes, namely classes annotated by kotlinx.serialization.Serializable, for which the SDK automatically manages serialization to and deserialization from JSON.

import kotlinx.serialization.Serializable

@Serializable
data class Contact(
    val birthday: String? = null,
    val firstName: String,
    val lastName: String,
    val phoneNumber: String? = null,
    val email: String? = null,
    val address: String? = null
)

val macca = Contact(
    birthday = "June 18, 1942",
    firstName = "Paul",
    lastName = "McCartney",
    phoneNumber = "020 1234 6541",
    email = "paulo@apple.com"
)
val maccaNode = manager / "contacts/macca" // get a reference on the data node representing the "macca" contact
maccaNode.set(macca)

The set() method accepts primitive types such as strings, numbers, booleans, arrays and dictionaries. In addition, and more interestingly, it also accepts classes that conform to the Codable protocol (and so automatically manage serialization and deserialization).

struct Contact: Codable { // our application-specific value type
  var birthday: String? = nil
  var firstName: String
  var lastName: String
  var phoneNumber: String? = nil
  var email: String? = nil
  var address: String? = nil
}
let macca = Contact(birthday: "June 18, 1942", firstName: "Paul", lastName: "McCartney", phoneNumber: "020 1234 6541", email: "paulo@apple.com")
let maccaNode = manager / "contacts/macca" // get a reference on the data node representing the "macca" contact
maccaNode?.set(macca)

The set method is implemented by the HTTP PUT method, with an application/json content type:

curl -X PUT https://io.datasync.orange.com/datasync/v2/<your-app>/data/contacts/macca -H 'Content-Type: application/json'
     -d '{"birthday":"June 18, 1942","firstName":"Paul","lastName":"McCartney","phoneNumber":"020 1234 6541","email":"paulo@apple.com"}' # our JSON object representing the contact details for "macca"

As the database relies on a tree-like structure, we could also have saved the contact properties at the "contacts" child node as a map or dictionary associating the "macca" key to the value of the previously specified contact:

const contactsNode = adbk.child("contacts");
contactsNode.set({
  macca: macca
});
val contactsNode = manager / "contacts"
contactsNode.set(mapOf("macca" to macca))
let contactsNode = manager / "contacts"
contactsNode?.set(["macca": macca])
curl -X PUT https://io.datasync.orange.com/datasync/v2/<your-app>/data/contacts -H 'Content-Type: application/json'
     -d '{"macca": {"birthday":"June 18, 1942","firstName":"Paul","lastName":"McCartney","phoneNumber":"020 1234 6541","email":"paulo@apple.com"}}'

Both examples result in the same database with the following content:

{
  "contacts": {
    "macca": {
      "birthday": "June 18, 1942",
      "firstName": "Paul",
      "lastName": "McCartney",
      "phoneNumber": "020 1234 6541",
      "email": "paulo@apple.com"
 	}
  }
}

If some data existed at the "contacts" node (typically some already existing contacts), then:

  • the first piece of code would overwrite only the "macca" sub-node, preserving the data of all other child nodes of the "contact" node.
  • the second piece of code would overwrite the node as a whole, erasing all pre-existent child nodes.

Merging data

The merge (or update) method makes it possible to update several child nodes of a given data node at a time. When called on a node, it overwrites the passed child nodes of this node and preserves any other already existing child nodes of this node. This preservation feature differentiates this method from the set one (which overwrites the whole node).

In the previous example, we can complete the "contacts" node instead of overwriting it:

The update() method works the same way as the set() one.

const lennon = { // our JSON object representing the contact details for a new "lennon" child node
   birthday: "October 9, 1940",
   firstName: "John",
   lastName: "Lennon",
   email: "johnandyoko@apple.com"
};
var contactsNode = adbk.child("contacts"); // get a reference on the "contacts" data node
contactsNode.update({
    lennon: lennon
});

The merge() method works the same way as the set() one.

val lennon = Contact(birthday = "October 9, 1940", firstName = "John", lastName = "Lennon", email = "johnandyoko@apple.com")
val contactsNode = manager / "contacts" // get a reference on the "contacts" data node
contactsNode.merge(mapOf("lennon" to lennon))

The merge() method works the same way as the set() one.

let lennon = Contact(birthday: "October 9, 1940", firstName: "John", lastName: "Lennon", email: "johnandyoko@apple.com")
let contactsNode = manager / "contacts" // get a reference on the "contacts" data node
contactsNode?.merge(with: ["lennon": lennon])

The merge method is implemented by the HTTP PATCH method, still with an application/json content type:

curl -X PATCH https://io.datasync.orange.com/datasync/v2/<your-app>/data/contacts -H 'Content-Type: application/json'
     -d '{"lennon": {"birthday":"October 9, 1940","firstName":"John","lastName":"Lennon","email":"johnandyoko@apple.com"}}'

As a result, when the set and merge examples are run sequentially, the database content looks like:

{
  "contacts": {
    "macca": {
      "birthday": "June 18, 1942",
      "firstName": "Paul",
      "lastName": "McCartney",
      "phoneNumber": "020 1234 6541",
      "email": "paulo@apple.com"
 	},
    "lennon": {
      "birthday": "October 9, 1940",
      "firstName": "John",
      "lastName": "Lennon",
      "email": "johnandyoko@apple.com"
 	}
  }
}

Using set and merge methods asynchronously

The set and merge methods actually work asynchronously and additionally either return a promise or accept a completion callback, which is completed or called when the write operation has been committed on the Webcom back end.

For example in the previous case, if you would like to know when your contact's data have been committed, you can use the returned promise or add a completion callback. When the data writing fails for some reason, the promise or the callback is rejected or called with an error describing the failure. When the data is successfully committed, the promise or the callback is completed or called with no error.

The JavaScript API works with promises:

contactsNode.set({macca: macca})
    .then(() => contactsNode.update({lennon: lennon})
        .then(() => alert("all contacts have been saved!"))
        .catch(error => alert("lennon contact could not be saved: " + error)))
    .catch(error => alert("macca contact could not be saved: " + error));

The Kotlin API works with callbacks of type (WebcomResult<Unit>) -> Unit:

contactsNode.set(mapOf("macca" to macca)) { // it: WebcomResult<Unit>
  when (it) {
    is WebcomResult.Failure -> println("macca contact could not be saved: ${it.error.message}")
    is WebcomResult.Success -> contactsNode.merge(mapOf("lennon" to lennon)) { // it: WebcomResult<Unit>
      when (it) {
        is WebcomResult.Failure -> println("lennon contact could not be saved: ${it.error.message}")
        is WebcomResult.Success -> println("all contacts have been saved!")
      }
    }
  }
}

The Swift API works with callbacks:

contactsNode?.set(["macca": macca]) { result in
    switch result {
    case let .failure(error):
        print("macca contact could not be saved:", error)
    case .success:
        contactsNode?.merge(with: ["lennon": lennon]) { result in
            switch result {
            case let .failure(error):
                print("lennon contact could not be saved:", error)
            case .success:
                print("all contacts have been saved!")
            }
        }
    }
}
Not applicable for REST API

Pushing data into lists

In our address book example, we could imagine that the address book is shared between several users. If a user adds a new contact it will be stored in the database. But in a shared address book many users may add contacts at the same time. If two users write simultaneously contacts into the "contacts" node, then one of the contact is likely to be deleted by the other.

To solve this problem, Webcom provides the push method. This method returns a new child node with a unique ID and sets some data under this newly created node (using the previously explained set method). The generated ID is based on a timestamp, so that the IDs of the of created child nodes are ordered chronologically. When using this method, several users can add children to the same data node of a database at the same time without any write conflict.

In the following example we show how to add contacts to our shared address book using the push method:

The push() method works the same way as the set() and update() ones.

const otherContactsNode = adbk.child("otherContacts");
otherContactsNode.push({
  birthday: "February 25, 1943",
  firstName: "George",
  lastName: "Harrisson",
  phoneNumber: "020 1234 8879"
});
otherContactsNode.push({
  birthday: "July 7, 1940",
  firstName: "Ringo",
  lastName: "Starr",
  phoneNumber: "020 1234 4561"
});

The push() method works the same way as the set() and merge() ones.

val otherContactsNode = manager / "otherContacts"
val contact1 = Contact(birthday = "February 25, 1943", firstName = "George", lastName = "Harrisson", phoneNumber = "020 1234 8879")
otherContactsNode.push(contact1)
val contact2 = Contact(birthday = "July 7, 1940", firstName = "Ringo", lastName = "Starr", phoneNumber = "020 1234 4561")
otherContactsNode.push(contact2)

The push() method works the same way as the set() and merge() ones.

let otherContactsNode = manager / "otherContacts"
let contact1 = Contact(birthday: "February 25, 1943", firstName: "George", lastName: "Harrisson", phoneNumber: "020 1234 8879")
otherContactsNode?.push(contact1)
let contact2 = Contact(birthday: "July 7, 1940", firstName: "Ringo", lastName: "Starr", phoneNumber: "020 1234 4561")
otherContactsNode?.push(contact2)

The push method is implemented by the HTTP POST method, still with an application/json content type:

curl -X POST https://io.datasync.orange.com/datasync/v2/<your-app>/data/otherContacts -H 'Content-Type: application/json'
     -d '{"birthday":"February 25, 1943","firstName":"George","lastName":"Harrisson","phoneNumber":"020 1234 8879"}'
     
curl -X POST https://io.datasync.orange.com/datasync/v2/<your-app>/data/otherContacts -H 'Content-Type: application/json'
     -d '{"birthday":"July 7, 1940","firstName":"Ringo","lastName":"Starr","phoneNumber":"020 1234 4561"}'

As a result, our database content now looks like (note the unique IDs generated by the push method):

{
  "otherContacts": {
    "-JtJIbH-AMSjUj-e-QAR": {
      "birthday": "February 25, 1943",
      "firstName": "George",
      "lastName": "Harrisson",
      "phoneNumber": "020 1234 8879"
    },
    "-JtJIbHINoEONq8fxNds": {
      "birthday": "July 7, 1940",
      "firstName": "Ringo",
      "lastName": "Starr",
      "phoneNumber": "020 1234 4561"
    }
  }
}

If you need to retrieve the generated ID, the push method returns a reference to the created child node:

// Generate a new contact node
var newContactNode = otherContactsNode.push({
    firstName: "Yoko",
    lastName: "Ono",
    phoneNumber: "020 1234 0999"
});

// Get the unique ID of this node
var newContactID = newContactNode.name();
val newContact = Contact(firstName = "Yoko", lastName = "Ono", phoneNumber = "020 1234 0999")
// Generate a new contact node
val newContactNode = otherContactsNode.push(newContact)
// Get the unique ID of this node
val newContactID = newContactNode.key
let newContact = Contact(firstName: "Yoko", lastName: "Ono", phoneNumber: "020 1234 0999")
// Generate a new contact node
let newContactNode = otherContactsNode?.push(newContact)
// Get the unique ID of this node
let newContactID = newContactNode?.name

Following recommendations for RESTful API, the actual path of the created child node is reported in the Location header of the HTTP response. For example:

> POST /datasync/v2/playground/data/otherContacts HTTP/1.1
> Host: io.datasync.orange.com
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 106
...
< HTTP/1.1 201 Created
< Content-Type: application/json
< Content-Length: 15
< Location: http://io.datasync.orange.com/datasync/v2/<your-app>/data/otherContacts/-JtJIbH-AMSjUj-e-QAR

It is also possible to retrieve the timestamp of a generated ID:

var newContactTimestamp = Webcom.pushIdToDateTime(newContactID); // expressed in milliseconds since Unix Epoch
console.log("Contact node created at", new Date(newContactTimestamp));
val newContactTimestamp: Date = newContactNode.timestamp
println("Contact node created at $newContactTimestamp")
let newContactDate = newContactNode.key.date
println("Contact node created at $date")
Not available in REST API