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 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":

// const app = Webcom.App("<your-app>"); // UNCOMMENT if you haven't yet an instance of your app!
const database = app.serverlessDb;

// get a reference to the node representing the address book
const adbk = database.rootNode;

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.relativeNode("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.relativeNode("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 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 merge() 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.relativeNode("contacts"); // get a reference on the "contacts" data node
contactsNode.merge({
    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.merge({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 a contact card at the same key under the "contacts" node, then one of the card will overwrite the other one.

To solve this problem, Webcom provides the push method. This method returns a new child node with a unique key and sets some data under this newly created node (using the previously explained set method). The generated key is based on a timestamp, so that the keys of the 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 merge() ones.

const otherContactsNode = adbk.relativeNode("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 keys 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"
    }
  }
}

Note that the push method returns the generated key of the created child node (either directly or in the resolved promise depending on the chosen Webcom SDK):

// Generate a new contact node
var newContactNode; 
otherContactsNode.push({
        firstName: "Yoko",
        lastName: "Ono",
        phoneNumber: "020 1234 0999"
    })
    // Get the unique ID of this node
    .then(key => newContactNode = key)
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: https://io.datasync.orange.com/datasync/v2/<your-app>/data/otherContacts/-JtJIbH-AMSjUj-e-QAR

It is also possible to generate by yourself a unique node key or to retrieve the timestamp of such a generated key:

var otherContactKey = database.generateUniqueKey()  // returns a String
var otherContactTimestamp = adbk.relativeNode(otherContactKey).timestamp; // returns a JavaScript Date
console.log("Contact node created at", otherContactTimestamp);
val otherContactNode = contactsNode.createChild() // to get the generated key, simply get the 'key' property
val otherContactTimestamp: Date = otherContactNode.timestamp // timestamp expressed as a java.util.Date
println("Contact node created at $otherContactTimestamp")
let otherContactNode = contactsNode?.createChild() // to get the generated key, simply get the 'name' property
let otherContactDate = otherContactNode?.key?.date // returns a Swift Date
println("Contact node created at $otherContactDate")
Not available in REST API

Removing data

Removing data at a given node is equivalent to set this node to null. For convenience, a clear method is provided.

// Remove "John" from the address book
contactsNode.relativeNode("John").clear()

// same result as: contactsNode.relativeNode("John").set(null)
// Remove "John" from the address book
(contactsNode / "John").clear()

// same result as: (contactsNode / "John").set(null)

// Remove "John" from the address book
(contactsNode / "John").clear()

// same result as: (contactsNode / "John").set(nil)

Updating a node at disconnection

By default, a write operation is executed immediately (more precisely, immediately in the local cache, and as soon as network connectivity is up on the Webcom back end).

It's also possible to schedule a write operation only at a network disconnection from the Webcom back end. This is specially useful to manage data representing presence of users in chat-like applications.
For example, when getting a network connection to the back end, the client app writes true at a "presence" node within the database and, in same time, it requests the back end to update this node to false as soon as the network connection is down, so that other clients see that the user becomes offline. Here is the code sample:

The optional parameter at, available on the set(), merge() and clear() methods, can be set to Webcom.ON_DISCONNECTION or Webcom.ON_DISCONNECTION (instead of the default value Webcom.NOW).

NEXT_DISCONNECTION will schedule the write operation only once at the next network disconnection, while ON_DISCONNECTION will schedule it repeatedly at each future network disconnection (until it is explicitly canceled using the cancelNextDisconnectionOps() method).

const chatUsers = database.rootNode.relativeNode("/users");
// let's consider the current user is "John"
userNode = chatUsers.relativeNode("John");

// add presence indication to user's details (the data will be actually written as soon as the back en is reachable)
userNode.merge({presence: true}, Webcom.NOW);
// schedule the update of the presence indication as soon as the back end becomes unreachable (the operation is sent
//         to the back end but will be performed only on network disconnection)
userNode.relativeNode("presence").set(false, Webcom.ON_DISCONNECTION);

// do some stuff
// ...
// then disconnect
database.disconnect();
// At this moment the presence is changed to 'false' both locally within client cache and remotely on the back end

Coming soon!
In the meanwhile, refer to the Android API reference

Coming soon!
In the meanwhile, refer to the iOS API reference