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!
// In this sample, you can use either `serverlessDb` or `serverlessDbLite`
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!")
}
}
}
}
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")
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:
This feature is not available on the "lite" version of the Serverless Database service.
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
Compare-and-set a node
This feature is available only on the lite Serverless Database service embedded in the Webcom SDK for JavaScript, and on the REST interface.
With the full version of the Serverless Database service, a notion of "transaction" is proposed, please refer to the Webcom SDK documentation.
The write operation is aborted by the Webcom back end if the actual value of the current data node is different from a known value given by the client. This results in a "compare-and-set" operation, which guarantees that data is really written only if the data node has not been modified in the meanwhile by another client.
// get John's profile details and store them in `currentValue`
let johnNode = adbk.relativeNode("contacts/John");
let currentValue;
johnNode.get().then(data => currentValue = data);
// ...do some stuff...
// update John's details only if unchanged
johnNode.set({fname: "John", age: 27}, currentValue)
.then(() => console.log('successfully updated'))
.catch(e => {
if (e.code === "HASH_DIFFERS") { console.log("node modified by someone else, update rejected!"); }
});
The following HTTP PUT
request attempts to update John's profile details, provided that the hash of the current
data node value equals the one passed to the hash
query parameter:
curl -X PUT https://io.datasync.orange.com/datasync/v2/<your-app>/data/contacts/John?hash=<calculatedHash>
-H 'Content-Type: application/json'
-d '{"firstName":"John", age: 27}' # our JSON object representing John's contact details
The hash calculation may be a bit complex, so you may have advantage to use the Webcom SDK for JavaScript which performs seamlessly this calculation for you.