The mobile service solution manages the synchronization of the offline data.
Requirements:
# Needs a form with name "offline_data"
# The form offline_data needs a method ws_read(version,name)
# If the data shown in mobile app is user specific the form offline_data needs a method ws_authenticate(useruid,password)
# The mobile_service and rest_ws plugin to be installed
By default Servoy will load the first 200 pks when a form is used. This is not needed in the service solution and can be prevented by calling databaseManager.setCreateEmptyFormFoundsets() in the solution onopen global method:
{code}
function onSolutionOpen() {
// prevent prefilling of form foundsets with default query
databaseManager.setCreateEmptyFormFoundsets()
}
{code}
h3. Constructing a offline data package for the mobile client
The ws_read(version,name) on the offline_data form has to return an OfflineDataDescription (=JSON) object filled with foundset data the developer wants the mobile client to retrieve.
An OfflineDataDescription instance is created with:
{code}
var retval = plugins.mobileservice.createOfflineDataDescription('data_');
{code}
Note: the argument is optional and is used to call rest endpoints for row data at forms starting with this prefix
Then we need to provide a list of relation names which should be used in traversal to find all data, like:
{code}
var traverse = new Array();
traverse.push('accountmanager_to_companies');
traverse.push('companies_to_contacts');
{code}
Lastly we have instruct the OfflineDataDescription to collect the data, starting with root foundset (containing records) and return.
{code}
retval.addFoundSet(fs_contact, traverse);
return retval;
{code}
Basically addFoundSet in the service solution exposes an unrelated foundset to the mobile client, which can be used in an unrelated way like in a (first) form or databaseManager.getFoundset(...)
For each record in the provided (unrelated) foundset the specified relations are traversed and all data taken.
h3. User specific data
In order to provide a mobile client with user specific data the ws_authenticate(useruid,password) method should be added:
{code}
function ws_authenticate(useruid,password)
{
//TODO find user and check password against pwhash column
if (password == 'demo')//static demo check
{
var retval = new Object();
retval.username = useruid;
return retval;
}
return false;
}
{code}
In the ws_read method we can utilize the authenticate username variable via
{code}
var authenticate_info = questionParams.ws_authenticate[0];
globals.username = authenticate_info.username;
{code}
Here the authenticate username is put into a global variable which in turn can be used like:
{code}
//prepare personal data
var fs_contact = globals.contact_data$username;//global related foundset using username global var, containing the account manager contact
{code}
Full ws_read method for personalized data.
{code}
function ws_read(version,name)
{
var questionParams = arguments[arguments.length-1];
//create return value
var retval = plugins.mobileservice.createOfflineDataDescription('data_');
//setting the key for user_select relation
var authenticate_info = questionParams.ws_authenticate[0];
globals.username = authenticate_info.username;
//prepare personal data
var fs_contact = globals.contact_data$username;//global related foundset using username global var, containing the account manager contact
/**
* @type {Array<String>}
*/
var traverse = new Array();
traverse.push('accountmanager_to_companies');
traverse.push('companies_to_contacts');
retval.addFoundSet(fs_contact, traverse);
return retval;
}
{code}
h3. Providing/retrieving entity(=table) row data
Row/record data is retrieved in separate calls for each entity, for example for "orders" row data results in a call to "orders" form is made on ws_read method.
Note: If a prefix is provided in the offlinedata the call will end up at prefix+entityname, example for prefix "data_" the call happens on form "data_orders"
ws_read is with a list of pks it wants as row data for, example code:
{code}
function ws_read(version,method)
{
var questionParams = arguments[arguments.length-1];
if (method == 'list')
{
/**
* @type {String}
*/
var ids = questionParams.ids[0];
if (ids != null && ids != '')
{
var idsa = ids.split(',', -1);
if (idsa.length > 0)
{
var json = plugins.mobileservice.getRowDescriptions(foundset.getDataSource(), idsa)
return json;
}
}
}
throw 404;
}
{code}
TIP: since ws_read for entities is likely the same, it might be beneficial to create a base form containing this logic and extend from this form
h3. Syncing back the changes from the mobile client
ws_update is called for changes made by mobile client, example code:
{code}
function ws_update(data,version,pk)
{
if (foundset.find())
{
foundset.contact_id = pk;
var count = foundset.search();
if (count > 0)
{
var rec = foundset.getRecord(1);
databaseManager.copyMatchingFields(data,rec,true);
databaseManager.saveData(rec);
}
}
}
{code}
ws_create is called for new records on the mobile client, example code:
{code}
function ws_create(data,version,pk)
{
var rec = foundset.getRecord(foundset.newRecord());
databaseManager.copyMatchingFields(data,rec,true);
databaseManager.saveData(rec);
}
{code}
Note: the retrieved PK (and derived FK's) is always UUID's if the underlaying datamodel is not UUID based, keep and apply a mapping\!
ws_delete is called for deleted record in the mobile client, example code:
{code}
function ws_delete(version,pk)
{
if (foundset.find())
{
var table = databaseManager.getTable(controller.getDataSource());
var pkname = table.getRowIdentifierColumnNames()[0]
foundset[pkname] = pk;
var count = foundset.search();
if (count > 0)
{
var rec = foundset.getRecord(1);
foundset.deleteRecord(rec);
}
}
}
{code}
h5. Transaction based syncing
By default the mobile client will do a rest call per changed row, these are separated calls so every call will be on a fresh new client on the serverside.
Because of this a transaction can't be used for all the changes of 1 mobile client and then fail a sync when something goes wrong. If the sync should be in 1 call, so that a transaction can be used over all the calls to the above ws_update/ws_create/ws_delete methods, a ws_update method must be created on the offline_data form.
This ws_update method then gets a full data package of all the changes that a mobile client has, this will then be dispatched over all the methods that are described above.
An example of a ws_update method on the offline_data form:
{code}
function ws_update(data,version,authenticateResult)
{
try {
databaseManager.startTransaction();
plugins.mobileservice.performSync(data,version,authenticateResult);
databaseManager.commitTransaction();
} catch (e) {
databaseManager.rollbackTransaction();
// log the error and return false to that the mobile client will know the sync did fail.
application.output(e,LOGGINGLEVEL.ERROR);
return false;
}
}
{code}
This code above starts a transaction then calls the performSync method of the mobile service plugin. This plugin will dispatch all the changes to the various ws_update/create/delete methods of the entity forms. If something goes wrong then an exception will be thrown and the transaction will rollback.
h5. Other pages
{list-siblings}\\ |