. . .

SDK Reference


This tutorial will guide you through the usage of our SDKs. Please read the Quickstart Guides to see how ApiOmat works. Our Cheat Sheet might also be helpful for a quick overview .

Quickstart Guides:

Cheat Sheets:



Content:


Module Setup


The SDKs are generated for each app individually and all of the SDK content is located in its own package / namespace ("com.apiomat.frontend" / "Apiomat", depending on the chosen SDK language). The operations you do within these objects and classes / prototypes (create, read, update, delete, referencing, etc.) are always the same.

For this guide we’ll create an app with the following classes and attributes:

  • Class: Conference

    • name (Text)

    • ranking (Floating point)

    • image (Image)

    • confSessions (Collection of ConfSession)

    • tags (Collection of Strings)

  • Class: Room

    • number (String)

    • floor (String)

    • capacity (Long)

  • Class: Person:

    • firstname (String)

    • lastname (String)

  • Class: ConfSession

    • name (String)

    • fromTime (Date)

    • toTime (Date)

    • i18n_name (Map)

    • conference(Conference)

    • handout(File)

    • room (Room as Embedded Object)

    • attendees (Persion as Embedded Object Collection)

Working with Users

A User is a client that sends requests to the backend that you created within ApiOmat. So an object of the class User is a unique user of your website, app, etc. A User usually logs into the backend.

We provide several methods by which you can manage your users. First of all, you have to configure the Datastore (or in Backbone.js: "authentication service") to be able to send requests with the user's credentials.Android

Android
User user = new User();
user.setUserName("user12");
user.setPassword("123asdf");
Datastore.configureWithCredentials(user);

C#

C#
User user = new User() { UserName = "user12", Password = "123asdf" };
Datastore.ConfigureWithCredentials(user);

Objective-C

Objective-C
AOMUser* user = [[AOMUser alloc] init];
[user setUserName:@"user12"];
[user setPassword:@"1a2s3d4f"];
[DataStore configureWithCredentialsWithUser:user configuration: nil];

Swift

Swift
let user = AOMUser()
user.userName = "yourName"
user.password = "yourPassword"
DataStore.configureWithCredentials(user: user)

TypeScript

TypeScript
const user = new User();
user.userName = "user12";
user.password = "123asdf";
Datastore.configureAsUser(user);

When using the Objective-C SDK, please don’t forget the import statements for the Datastore (AOMDatastore.h) and User (AOMUser.h) class.

in addition to the user's credentials, you can configure the Datastore with custom parameters like baseurl, apikey and the used system (LIVE, STAGING, TEST).


Objective-C

Objective-C
AOMUser* user = [[AOMUser alloc] init];
[user setUserName:@"user12"];
[user setPassword:@"1a2s3d4f"];
 
DataStoreConfiguration *config = [DataStoreConfiguration new];
[config setBaseUrl: @"http://localhost:8080/yambas/rest/apps/Conference"];
[config setApiKey: @"1234567890"];
[config setUsedSystem: @"LIVE"];
 
[DataStore configureWithCredentialsWithUser: user configuration: config];

Swift

Swift
let user = AOMUser()
user.userName = "yourName"
user.password = "yourPassword"
let configuration = DataStoreConfiguration()
configuration.apiKey = "1234567890"
configuration.baseUrl = "http://localhost:8080/yambas/rest/apps/Conference"
configuration.usedSystem = "LIVE"
 
DataStore.configureWithCredentials(user: user, configuration: configuration)


Now your connection to our servers is initialized and you can start by logging in or signing up.

To sign up with a new user, call the save function on the previously created user object. Some SDKs offer a synchronous and an asynchronous way to do this. In many frontends a synchronous function call blocks the UI thread, which in case of a function call which leads to requests to the server over the network might lead to a UI that appears to be frozen for a short time, which again leads to a bad user experience, so it's not recommended to do this. See further below for the asynchronous function calls.Android

Android
try {
user.save();
} catch (ApiomatRequestException e) {
// Handle exception
}

C#

C#
user.SaveAsync().Wait();


Here are the asynchronous method calls for the same task:Android

Android
user.saveAsync( new AOMEmptyCallback(){
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception){}
});

C#


C#
await user.SaveAsync();


Objective-C

Objective-C
[user saveObjCWithLoadAfterwards:true completion:^(NSError * error) {
if(error)
{
/* request was not successful */
}
else
{
/* sign up was successful */
}
}];

Swift

Swift
user.save(loadAfterwards: true, completion: { error in
if error != nil {
//handle error
}
})

TypeScript

TypeScript
try {
await user.save();
} catch (e) {
//do error handling
}


If the user already exists you can log in the user instead:Android

Android
try {
user.loadMe();
} catch (ApiomatRequestException e) {
// Handle exception
 
}

C#

C#
try
{
await user.LoadMeAsync();
}
catch (ApiomatRequestException ex)
{
// Handle exception
 
}

Objective-C

Objective-C
[user loadMe:^(NSError * error) {
if(error != nil) {
//handle error
}
}];

Swift

Swift
user.loadMe { error in
if error != nil {
//handle error
}
}

TypeScript

TypeScript
try {
await user.loadMe();
} catch (e) {
// do error handling
}


In many cases you might want to combine the two methods above - you want to try logging the user in, and if this leads to an error because the user doesn't exist yet, sign him up (create the account). There is a common pattern for doing that:Android

Android
final User user = new User();
user.setUserName("_username_");
user.setPassword("_password_");
Datastore.configureWithCredentials(user);
user.loadMeAsync(new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
if (exception != null) {
// if the member is not found on the server just create it
user.saveAsync(new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});
}
}
});

C#

C#
private async Task<User> LogInOrSignUpUserAsync(string userName, string password)
{
User user = new User() { UserName = userName, Password = password };
Datastore.ConfigureWithCredentials(user);
bool userExists = false;
try
{
await user.LoadMeAsync();
userExists = true;
}
catch (ApiomatRequestException e)
{
if (e.Status == Status.UNAUTHORIZED)
{
userExists = false;
}
else
{
throw e;
}
}
if (!userExists)
{
// Regarding this saving of the user outside of the previous catch block, please read the section "Error handling" in the documentation below.
await user.SaveAsync();
}
// only needed if you want to use the user to configure the Datastore again later
// user.Password = password;
return user;
}

Objective-C

Objective-C
AOMUser* user = [[AOMUser alloc] init];
[user setUserName:@"yourName"];
[user setPassword:@"yourPassword"];
[DataStore configureWithCredentialsWithUser: user configuration: nil];
[user loadMe:^(NSError * error):^(NSError *error) {
if([error code] == AOMUNAUTHORIZED)
{
[user saveObjCWithLoadAfterwards:true completion:^(NSError * error) {
}];
}
}];

Swift

Swift
let user = AOMUser()
user.userName = "yourName"
user.password = "yourPassword"
DataStore.configureWithCredentials(user: user)
user.loadMe { error in
if error != nil {
user.save(loadAfterwards: true, completion: { error in
if error != nil {
//handle error
}
})
//handle error
}
}

TypeScript

TypeScript
async function logInOrSignUpUserAsync(username: string, password: string) {
const user = new User();
user.userName = username;
user.password = password;
Datastore.configureAsUser(user);
 
let userExists = false;
try {
await user.loadMe();
userExists = true;
} catch (error) {
if (error.statusCode == AOMStatus.UNAUTHORIZED) {
userExists = false;
} else {
// do error handling
}
}
if(userExists === false){
await user.save();
}
}

Be aware that the password field of the user object is empty after loading and saving, because the password doesn’t get transferred from the server. This is not an issue because at the beginning of the method the Datastore already got configured and all following requests have the correct credentials. But in case you want to configure the Datastore to something else and then back to the user, you might want to store the password somewhere safe, or set it as value of the password attribute afterwards.

Using OAuth2

For authenticating users, you can use our OAuth2 implementation. Read the dedicated documentation page to learn more about it: OAuth2.

Handling Objects

Let’s have a look at how we work with plain objects and do CRUD operations on them. Every object in our system has an ID, which is also part of the object's HREF. This ID and URL identify the object.

Depending on the SDK, there are synchronous and asynchronous functions to send requests to the server. Remember, synchronous function calls usually block the UI thread and are not recommended. The asynchronous function need a way to return a value - this is solved either with callbacks / blocks / completion handlers or with the async/await pattern.

For some SDKs the callback handling requires more explanation, so here's some code that shows you how a callback generally looks like:JavaScript

JavaScript
{
onOk: function([result]) {
//Request successful
//If server returns anything than you will find this in the result variable
},
onError: function(error) {
/*
Request failed
the parameter error contains more information
see also section about Error handling below
*/
}
}


Handling Objects

The following section will show you how to create, read, update and delete objects synchronously and asynchronously in the different SDKs.

Create an object

Saving an object in the backend always follows the same pattern:Android

Android
Conference conference = new Conference();
conference.setName("Our first conference");
// We give the save command an additional parameter which is an new object of class AOMEmptyCallback. The isDone method will be executed after the request is finished. You can check the error parameter to see if something went wrong.
conference.saveAsync(new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception){}
});

C#

C#
Conference conference = new Conference();
conference.Name = "Xamarin Evolve 2014";
// In C# there are no checked exceptions like there are in Java, so you don't have to try/catch here, but it's advised to do so. This will be covered in more detail in the documentation below under the topic "Error handling".
await conference.SaveAsync();

Objective-C

Objective-C
Conference *conf = [[Conference alloc] init];
[conf setName:@"Our first conference"];
/* set some other attributes here */
/* We give the save command a parameter of type "AOMEmptyBlock" (AOMEmptyBlock is only a type definition). This block will be executed after the request is finished. You can check the error parameter to see if something went wrong */
[conf saveObjCWithLoadAfterwards:true completion:^(NSError *error) {
if(!error)
{
/* everything was ok */
}
else
{
/* sth went wrong */
}
}];

Swift

Swift
let conf = Conference()
conf.name = "our first conference"
/* set some other attributes here */
conf.save { error in
if error == nil {
//everything okay
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
const conference = new Conference();
try {
await conference.save();
} catch (error) {
// do error handling
}

Read an object

To get a single object you have to know its unique identifier (the HREF). By using following command you are able to get a specific object.Android

Android
Conference conference = new Conference();
conference.loadAsync(href, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//Now you're holding your object.
}
});

C#

C#
Conference conference = new Conference();
await conference.LoadAsync(href);

Objective-C

Objective-C
NSString *href = @"https://apiomat.org/yambas/rest/apps/Conference/models/ConferenceMain/Conference/0123456";
Conference *conf = [[Conference alloc] init];
[conf loadObjCWithHref:href completion:^(NSError *error) {
/* Now you're holding your object. */
}];

Swift

Swift
let conf = Conference()
conf.load(withHref: href) { error in
if error == nil {
//successfully reloaded your conference
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
const conference = new Conference();
await conference.loadWithHref(href);


If you need to reload the data from server, then type in the following:Android

Android
loadedConf.loadASync(new AOMEmptyCallback(){
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//you reloaded your object
}
});

C#

C#
await loadedConf.LoadAsync();

Objective-C

Objective-C
[loadedConf loadObjCWithCompletion:^(NSError *error) {
/* you reloaded your object */
}];

Swift

Swift
conf.load { error in
if error == nil {
//successfully reloaded your conference
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
await loadedConf.load();

Read a list of objects

You can also retrieve a list of objects from the server. To do so you have to work with the class / prototype the objects are instances of.Android

Android
conference.getConferencesAsync(null, new AOMCallback<List<Conference>>() {
@Override
public void isDone(List<Conference> resultObject, boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
IList<Conference> loadedConferences = await Conference.GetConferencesAsync();

Objective-C

Objective-C
/* Load list of conference objectse */
[Conference loadListWithQuery:@"" completion:^(NSArray<AbstractClientDataModel *> * loadedConfs, NSError *error) {
/* Getting list was successful if error == null */
/* You can now work with parameter loadedConfs */
}];

Swift

Swift
/* Load list of conference objects */
Conference.loadList { conferences, error in
if error == nil {
let loadedConferences = conferences
} else {
//something went wrong, handle errror
}
}

TypeScript

TypeScript
const conferences = await Conference.getConferences();


Instead of no parameter or ‘null’ (depending on the SDK) you can also add a query to filter the objects that get returned by the server.C#

C#
IList<Conference> loadedConferencesFiltered = await Conference.GetConferencesAsync("name==\"Microsoft Build 2014\"");

Objective-C

Objective-C
/* Load list of conference objects filtered by name */
[Conference loadListWithQuery:@"name like \"Backend\"" completion:^(NSArray<AbstractClientDataModel *> * loadedConfs, NSError *error) {
/* Getting list was successful if exception == null */
/* You can now work with parameter loadedConfs which contains filtered elements */
}];

Swift

Swift
/* Load list of conference objects filtered by name */
Conference.loadList("name like \"backend\"") { conferences, error in
if error == nil {
let loadedConferences = conferences
} else {
//something went wrong, handle errror
}
}

TypeScript

TypeScript
const conferences = await Conference.getConferences("name==\"Microsoft Build 2014\"");

You can find some more information about queries in the respective section on this page, or check out the dedicated documentation article about our query language.

Read the count of an object list

In case you just want to know the count of elements in a list (either filtered by a query or not), you don't have to fetch the full list to then call some count() method in your code. Instead, you can call the count() method in our SDK, so that the server responds with the count of elements directly:Android

Android
//Load count of conference objects filtered
conference.getConferencesCountAsync("name like \"JavaOne\"", new AOMCallback<Long>() {
 
@Override
public void isDone(Long resultObject, boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
long conferenceCount = await GetConferencesCountAsync(); // Counts all conferences
long conferenceCountFiltered = await GetConferencesCountAsync("name like \"JavaOne\""); // Counts filtered conferences

Objective-C

Objective-C
__block NSNumber *cnt = nil;
__block NSError *err = nil;
[Conference loadCountObjCWithQuery:@"name like \"Steve\"" completion:^(NSNumber *count, NSError *error) {
cnt = count;
err = error;
}];

Swift

Swift
// Count all conferences
Conference.loadCount { conferencesCount, error in
if error == nil {
if let conferencesCount = conferencesCount {
// work with conferencesCount
}
} else {
// handle error
}
}
 
// Count filtered conferences
Conference.loadCount(query: "name like \"Steve\"") { conferencesCount, error in
if error == nil {
if let conferencesCount = conferencesCount {
// work with conferencesCount
}
} else {
// handle error
}
}

TypeScript

TypeScript
const conferenceCount = await Conference.getConferencesCount(); // Counts all conferences
const conferenceCountFiltered = await Conference.getConferencesCount("name like 'TypeScript'"); // Counts filtered conferences

Update an object

It’s identical to saving. The client code checks, if the object already contains an ID / HREF and afterwards performs an update. For example:Android

Android
// Get or save updatedObj
// Change sth on updatedObj
conference.saveAsync( new AOMEmptyCallback(){
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//Successfully updated
}
});

C#

C#
// Get or save a conference
// ...
// Change something on the conference
// ...
// Update the conference
await conference.SaveAsync();

Objective-C

Objective-C
/* Get or save updatedObj */
/* Change sth on updatedObj */
/* udpate it on server */
[updatedObj saveObjCWithLoadAfterwards:true completion:^(NSError *error) {
}];

Swift

Swift
/* Get or save updatedObj */
/* Change something on updatedObj */
/* udpate it on server */
updatedConf.save { error in
if error == nil {
//everything okay
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
// Get or save updateObj
// Change sth on updateObj
await updateObj.save();

Delete an object

According to our pattern, you can delete an object simply by entering the following:Android

Android
// Load object you want to delete
conference.deleteAsync(new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
await conference.DeleteAsync();

Objective-C

Objective-C
//Load object you want to delete
[conf deleteObjCWithCompletion:^(NSError *error) {
/* check error if sth went wrong */
}];

Swift

Swift
//First load object you want to delete (e.g. a conference), then call
conf.delete { error in
if error == nil {
//successfully deleted
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
await conference.delete();

Offline Handling

We extended the offline handling capabilities and have a dedicated documentation page now. See offline handling.

DeltaSync

For reducing the amount of data being sent between the device and backend, you can enable DeltaSync. Read the dedicated documentation page to learn more about it: DeltaSync.

Working with attributes of objects

Of course you can access attributes that you’ve created in your dashboard the standard Java / C# / Objective-C / Swift / etc way. Please be careful to use the appropriate data type. It’s not recommended to add new attributes to generated classes, because it will break serialisation if you call the “save()” method to send an instance of that class to the server.

Handling Object Attributes

Setting an attribute

Assuming our conference class has an attribute called ‘name’ , you can set this property easily with:Android

Android
Conference conference = new Conference();
conference.setName("Android Dev Con");
//save the object

C#

C#
// Either with an object initializer
Conference conf1 = new Conference() { Name = "Microsoft Build 2014" };
// Or with the property of the object
Conference conf2 = new Conference();
conf2.Name = "Apps World Europe 2014";
// don't forget to save the objects

Objective-C

Objective-C
Conference *conf = [[Conference alloc] init];
[conf setName:@"Worldwide Developers Conference"];

Swift

Swift
var conf = Conference()
conf.name = "Worldwide Developers Conference"

TypeScript

TypeScript
const conference = new Conference();
conference.name = "TypeScript Dev Conf";

or set an attribute “someMap” of ApiOmat type map, which looks like this:Android

Android
Map<String, String> map = new HashMap<String,String>();
map.put("de", "Wie benutze ich properties?");
map.put("en", "How to use properties");
ConfSession confSession = new ConfSession();
confSession.setI18n_name(map);

C#

C#
// Either with a dictionary initializer
IDictionary dict = new Dictionary<string,string< ()
{
{"de", "Wie benutzt man Properties?"},
{"en", "How to use properties?"}
};
// or after the creation
dict.Add("fr", "Comment utiliser les propriétés");
// then set the conference's property accordingly
ConfSession confSession = new ConfSession();
confSession.I18n_name = dict;

Objective-C

Objective-C
NSMutableDictionary *i18n = [[NSMutableDictionary alloc] init];
[i18n setObject:@"wie benutze ich properties" forKey:@"de"];
[i18n setObject:@"how to use properties" forKey:@"en"];
Session *session = [[Session alloc] init];
[session setI18n_name:i18n];

Swift

Swift
var i18n = [String: String]()
i18n["de"] = "wie benutze ich Properties"
i18n["en"] = "how to use properties"
let session = Session()
session.i18n_name = i18n as [String : AnyObject]

TypeScript

TypeScript
confSession.i18n_name = {
de: "Wie benutze ich Properties",
en: "How to use properties"
};
await confSession.save();

Getting an attribute

Like accessing “normal” attributes, you can also access properties of ApiOmat classes.Android

Android
//Make sure you loaded object conference before
String confName = conference.getName();

C#

C#
// Make sure you loaded the conference object before
string confName = conference.Name;

Objective-C

Objective-C
/* Make sure you loaded object conference before (see above) */
NSString *confName = [conference name];

Swift

Swift
/* Make sure you have loaded object conference before (see above) */
let confName = conf.name

TypeScript

TypeScript
const confName = conference.name;

Activate attribute validation

With Yambas 3.3 it is possible to set additional options for an attribute like regular expression, minimum and maximum length. You can activate client side validation with: Android

Android
//default is false
Datastore.getInstance().setValidateAttributes(true);

TypeScript

TypeScript
//default is false
Datastore.Instance.validateAttributes = true

Encrypting an attribute

With Yambas 3.4 it's possible to use client-side encryption to encrypt Strings within the SDK. To approach this the attribute of the MetaModel needs to be a String and needs to be marked as encrypted, see @com.apiomat.nativemodule.EncryptAttribute within the Development Basics.
Once an attribute was set to encrypted the getter and setter methods are automatically enhanced to encrypt and decrypt their values.

The used encryption algorithm depends on the specific SDK implementation and requires at least an additional secret passphrase which can be set to the datastore within the SDK.


Keep in mind that setting the MetaModelAttribute to encrypted after some data objects already exist, may result in problems when accessing the existing data objects.

As soon as the attribute is set to encrypted the SDKs that implementing this feature will try to decrypt the attribute values even if they weren't encrypted before. This may result in decryption errors when accessing the attribute values via getter.


Also we recommend to use a password managment software like keychain to store your secret passphrases.


The client-side encryption is available for the following SDKs:


TypeScript

TypeScript
// the symmetric encryption algorithm AES is used in TS SDK
// The length of the used key depends on the length of the secret. AES-256 is used if the secret is a passphrase.
// Check https://github.com/brix/crypto-js for more details
 
...
// set encryption secret to dataStore
const secret = "my secret passphrase";
Datastore.Instance.setEncryptionSecret( secret );
 
 
// encrypt plain text and save cipher text to class instance
const myClass = new MyClassWithEncryptedAttribute();
myClass.encryptedAttribute = "plain text";
await myClass.save();
 
 
// load cipher text and decrypt to retain the actual plain text
const myLoadedClass = new MyClassWithEncryptedAttribute();
await myLoadedClass.load( myClass.href );
const decryptedValue = myLoadedClass.encryptedAttribute;


Working with geo points

You can also set ‘Location’ as type of an attribute. To save an attribute of the type “location” with name ‘place’, do:Android

Android
conference.setPlaceLatitude(10.2);
conference.setPlaceLongitude(20.2);
//To retrieve coordinates, you can call the methods 'getLatitude' and 'getLongitude'.
double latitude = conference.getPlaceLatitude();
double longitude = conference.getPlaceLongitude();

C#

C#
conference.PlaceLatitude = 10.2;
conference.PlaceLongitude = 20.3;
// Access the properties in the same way
double latitude = conference.PlaceLatitude;
double longitude = conference.PlaceLongitude;

Objective-C

Objective-C
[conf setPlaceLatitude:10.2];
[conf setPlaceLongitude:20.2];
//To retrieve coordinates,
//you can call the methods 'latitude' and 'longitude'.
double latitude = [conf getPlaceLatitude];
double longitude = [conference getPlaceLongitude];

Swift

Swift
conf.place?.longitude = 10.2
conf.place?.latitude = 20.2
//To retrieve coordinates,
//you can access the corresponding attributes of your your location-typed property
let confLng = conf.place?.longitude
let confLat = conf.place?.latitude

TypeScript

TypeScript
const location: AOMLocation = {
latitude: 20.5,
longitude: 30.9,
};
conference.place = location;

Working with files

If you have an attribute of the type “File” you can upload a file either as a byte array or as stream (or in Android directly as "File"). The method which uploads your file is called post<Attributename>. For our “handout” in the ConfSession class it should look like this:


Android

Android
// As byte array:
byte[] filearr = {0,3,6,2};
ConfSession confsession = new ConfSession();
confsession.postHandoutAsync(filearr, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});
 
// As stream (for streams where mark and reset is supported)
InputStream inputStream = new ByteArrayInputStream(filearr);
ConfSession confsession = new ConfSession();
confsession.postHandoutAsync(inputStream , new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
}); 
 
// As file
File file = new File(path);
ConfSession confsession = new ConfSession();
confsession.postHandoutAsync(file , new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});  

C#

C#
// As byte array:
byte[] fileArr = new byte[] {0x00, 0x30, 0x60, 0x20};
ConfSession confSession = new ConfSession();
await confSession.PostHandoutAsync(fileArr);
 
// As stream (this could be any kind of stream, especially useful when working with files from the device like photos or videos):
MemoryStream stream = new MemoryStream(fileArr);
await confSession.PostHandoutAsync(stream);

Objective-C

Objective-C
// As NSMutableData
NSMutableData *fileData = [[NSMutableData alloc] init];
/* set value for property fileData */
ConfSession *confSession = [[ConfSession alloc] init];
[confSession postHandoutObjC:fileData completion:^(NSError *error) {
}];
 
// As stream (this could be any kind of stream, especially useful when working with files from the device like photos or videos):
NSInputStream* stream = [NSInputStream inputStreamWithData: fileData];
[confSession postHandoutStreamObjC: stream completion:^(NSError *error) {
if(error == nil)
{
// data successfully posted
}
else
{
// something went wrong, handle error
}
}];

Swift

Swift
// As byte array
let data = NSMutableData()
let confSession = ConfSession()
confSession.postHandout(data) { error in
if error == nil {
//data successfully posted
} else {
//something went wrong, handle error
}
}
 
// As stream (this could be any kind of stream, especially useful when working with files from the device like photos or videos):
let stream = file as Data?
confSession.postHandout(stream!) { error in
if error == nil {
// data successfully posted
} else {
// something went wrong, handle error
}
}

TypeScript

TypeScript
// _data is a byte-array containing the example data
const _data = [0,3,6,2];
// session is an object of a class that has a file attribute "slides"
await session.postSlides(_data);

Please note two things:

1) There's an upload size limit on the server. It can be configured in the apiomat.yaml and defaults to 200 MB.

2) There's a limit for offline storage in the device. The limit gets ignored when uploading while the device is online (nothing gets stored this way anyway), but an exception gets thrown if the device is offline. When downloading a file, it only gets cached (either in memory or persistently, depending on the setting) if the limit isn't exceeded, otherwise an error will be logged (but no exception thrown). This limit can be configured via the Datastore and defaults to 15 MB.


After the file object was uploaded, the property <property>URL contains an URL to this file.Android

Android
string slidesUrlPlain = conference.getSlidesResourceURL(); // Without the addition of API key and system
string slidesUrl = conference.getSlidesUrl(); // With the addition of API key and system

C#

C#
string slidesUrlPlain = conference.SlidesURLResource; // Without the addition of API key and system
string slidesUrl = conference.SlidesUrl; // With the addition of API key and system

Objective-C

Objective-C
NSString *handoutURL = [confSession handoutURL];

Swift

Swift
let handoutURL = confSession.handoutURL

TypeScript

TypeScript
const slidesURL = session.slidesURL;


To load the file, you can either use a method on the object that contains the file directly, or if you only have a URL of the file and not the object that contains the file you can use a method in the Datastore, called loadResourceAsyncWithHref().

Android

Android
// Via the object that contains the file:
byte[] result = session.LoadSlidesAsync(new AOMEmptyCallback() { // usePersistentStorage is optional
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});
 
 
/* Indicating whether the loaded image should be cached using the persistent storage or not,
for more information have a closer look at the caching topic */
boolean usePersistentStorage = false;
 
// Via Datastore when you only have a file's URL
byte[] result2 = await Datastore.getInstance().LoadResourceAsync(fileUrl, false, usePersistentStorage, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
// Via the object that contains the file:
byte[] result = await session.LoadSlidesAsync(); // usePersistentStorage is optional
 
// Via Datastore when you only have a file's URL
byte[] result2 = await Datastore.Instance.LoadResourceAsync(fileUrl, false, false, usePersistentStorage);

Objective-C

Objective-C
NSString *handoutURL = [confSession handoutURL];
[[Datastore sharedInstance] loadResourceWithHref:handoutURL usePersistentStorage:false completion:^(NSData *data, NSError *error) {
/* data object contains your file data */
}];
 
 
/* Alternatively you can use the following method as well */
[confSession loadHandoutObjCWithCompletion:^(NSData *data, NSError *error) {
/* data object contains your file data */
}];

Swift

Swift
let handoutURL = confSession.handoutURL
DataStore.sharedInstance.loadResource(withHref: handoutURL, usePersistentStorage: false) { data, error in
if error != nil {
/* data object contains your file data */
}
}
 
/* Alternatively you can use the following method as well */
confSession.loadHandout { (data, error) in
if error != nil {
/* data object contains your file data */
}
}

TypeScript

TypeScript
/* you can either load it via Datastore */
const data = await Datastore.Instance.loadResource(session.slidesURL);
 
/* Or alternatively you can use the following method as well */
const loadedSlides = await session.loadSlides();

Working with images

It’s possible to add a property of type “image” to your classes. If you do this, you can find some special methods in the generated code for these classes. Upload an image to an instance of your class as a byte array / array buffer. The method which adds an image is named after the pattern “Post<Propertyname>”. For our “Conference” class and its property “image” it should look like in following snippet:


Android

Android
byte[] imgArr = new byte[] {0,2,6,8};
conference.postImageAsync(imgArr, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
byte[] fakeImage = new byte[] {0x00, 0x30, 0x60, 0x20};
ConfSession confSession = new ConfSession();
await confSession.PostImageAsync(fakeImage);

Objective-C

Objective-C
 NSMutableData *imageData = [[NSMutableData alloc] init];
/* set value for property imageData */
/* save data in image property of conf object */
[conf postImageObjC:imageData completion:^(NSError *error) {
/* check error parameter */
}];

Swift

Swift
let data = NSMutableData()
let conf = Conference()
conf.postImage(data) { error in
if error == nil {
//data successfully posted
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
const imageData = [0,2,5,3]; // image data as byte array
const conference = new Conference();
await conference.postImage(imageData);


If the image was uploaded, the property ‘<PropertyName> URL’ contains an URL to the image. To get this image URL back from your object, you can do the following:Android

Android
//Get plain URL without concerting parameters
String imgUrl = test.getImageURL();
//Get URL with resized image to 200x100, black background, no transparency and in format jpg
String resizedImgUrl = test.getImageURL(200, 100, "ffffff", null, "jpg");

C#

C#
// 1) Get plain URL without concerting parameters from the property
string imgUrl = conference.ImageUrl;
// 2) Get URL with resized image to 200x100, black background, no transparency and as jpg
string resizedImgUrl = conference.GetImageUrl(200, 100, "ffffff", null, "jpg");

Objective-C

Objective-C
[conf loadScaledImageObjCWithWidth:200 height:100 backgroundColorAsHex:@"ffffff" alpha:0 format:@"jpg" completion:^(NSData * data, NSError * error) {
if(error == nil)
{
//Successfully load scaled image
}
else
{
//error handling
}
}];

Swift

Swift
conf.loadScaledImage(width: 200, height: 100, backgroundColorAsHex: "ffffff", alpha: 0, format: "jpg") { data, error in
if error == nil {
let imageData = data
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
//Get plain URL without concerting parameters
const imgUrl = conference.imageURL;
//Get URL with resized image to 200x100
const resizedImgUrl = conference.getImageURL(200,100);


If you only set either the width or height parameter, the image will be resized while the scale will be kept (the other parameter will be calculated accordingly).

When using the Objective-C SDK, there are 2 possibilities to download an image:

  • Use your own NSURLConnection or anything else

  • Use loadResourceAsyncWithHref of DataStore class (see section above for example)

When using a file URL in a web page you can add the URL parameter asstream=true. This changes the file handling and enables the following behaviour:

  • When clicking on the link and the file is for example a PDF, the browser detects the file type correctly and can show the PDF directly

  • When downloading the file

    • Your OS detects the file type and can propose to open the file with a program that supports the file type

    • When saving, the proposed name includes the file type, e.g. "report.pdf" instead of just "report"


Working with One/Limited Time Access Links

The following examples show how to generate limited-access and one-time links with the help of the SDK:TypeScript

TypeScript
//Get token URL for file, which is valid for 600 seconds (10 minutes)
const fileTokenUrl = await conference.getFileTokenURL(600);
//Get token URL for file, which is valid for 600 seconds and can only be used once
const fileTokenUrlOneTime = await conference.getFileTokenURL(600, true);
 
//Get token URL for image, which is valid for 600 seconds (10 minutes)
const imgTokenUrl = await conference.getImageTokenURL(600);
//Get token URL for image, which is valid for 600 seconds and can only be used once
const imgTokenUrlOneTime = await conference.getImageTokenURL(600, true);


Handling References

By adding attributes containing references, you are able to build data structures in an object oriented manner. You can reference a single or a list of classes.

You can add a list of type ConfSession to your Conference class or set a Conference as a reference of the ConfSession class, for example. This would resemble a one-to-many relationship: [Conference]-1—n-[ConfSession].

The generated SDKs will contain several methods starting with ‘post..’, ‘load…’ or ‘remove…’ afterwards.

Create a reference

Our example Conference class contains a property ‘confSessions’ which is of type ‘Collection of ConfSession’. To add a ConfSession to an object of type Conference you can do the following:Android

Android
//Create and save an object of type ConfSession (see above)
//Create and save an object of type Conference (see above)
conference.postConfSessionAsync(confSession, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
// Create and save an object of type ConfSession (see above on how to do that)
// Create and save an object of type Conference (see above on how to do that)
await conference.PostConfSessionAsync(confSession);

Objective-C

Objective-C
/* Create and save an object (called confSession) of type ConfSession (see above) */
/* Create and save an object (conference) of type Conference (see above) */
[conference postConfSessionsObjC:confSession completion:^(NSString * href, NSError * error) {
}];

Swift

Swift
/* Create and save an object (called confSession) of type ConfSession (see above) */
/* Create and save an object (conference) of type Conference (see above) */
conference.postConfSessions(confSession) { href, error in
if error == nil {
//confSession successfully posted, href contains the href to the posted reference
} else {
//something went wrong, handle error
}
};

TypeScript

TypeScript
//Create and save an object of type ConfSession (see above)
//Create and save an object of type Conference (see above)
await conference.postConfSessions(confSession);

Read references

To retrieve the referenced object(s) you have to call the “load” function for this property. This function downloads the object(s) from the backend. Depending on the SDK, you can either get the loaded objects from the return value or from a property that stores the values after they have been loaded:Android

Android
//Assume that our object conference has already referenced confSessions
conference.loadConfSessionAsync(null, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//if there is no exception you can access the local list on conference object
List<confsession> sessions = conference.getConfSessions();
}
});

C#

C#
// Assume that our object conference already contains referenced confSessions
// either use the return value
IList<ConfSession> sessions = await conference.LoadConfSessionAsync();
// or the property
sessions = conference.ConfSessions;

Objective-C

Objective-C
/* Assume that our object conference has already referenced confSessions */
[conference loadConfSessionsObjC:@"" completion:^(NSArray<AbstractClientDataModel *> * models, NSError * error) {
/* if there is no exception you can access the local list on conference object*/
NSMutableArray *loadedSessions = [conference sessions];
}];

Swift

Swift
/* Assume that our object conference has already referenced confSessions */
conference.loadConfSessions { sessions, error in
if error == nil {
let loadedSessions = sessions
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
//Assume that our object conference has already referenced confSessions
const confSessions = await conference.loadConfSessions();
//Or use the property if the sessions are already loaded
const loadedSessions = conference.confSessions;


Instead of ‘null’ / 'undefined' / empty parameter list you can set a query parameter to filter the list. For this please see the Query section below or the documentation article about Queries.

Update a reference

If you change properties of an object which is already referenced to another object and send an update to the server, than the reference will also be changed. If you call “load” again you will retrieve the updated reference.

Hint: If you add an existing reference to the same object again, it will not be added as new reference again.

Delete reference

Delete a reference between objects is straightforward.Android

Android
//Assume that conference contains a reference to confSession
//Assume that both objects (conference, confSession) are loaded from server
conference.removeConfSessionAsync(confSession, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
}
});

C#

C#
// Assume that conference contains a reference to confSession
// Assume that both objects (conference, confSession) are loaded from server
await conference.RemoveConfSessionAsync(confSession);

Objective-C

Objective-C
/* Assume that conference contains a reference to confSession */
/* Assume that both objects (conference, confSession) are loaded from server */
[conference removeConfSessionsObjC:confSession completion:^(NSError *error) {
}];

Swift

Swift
/* Assume that conference contains a reference to confSession */
/* Assume that both objects (conference, confSession) are loaded from server */
conference.removeConfSession(confSession) { error in
if error == nil {
//confSession successfully removed
} else {
//something went wrong, handle error
}
}

TypeScript

TypeScript
// Assume that conference contains a reference to confSession
// Assume that both objects (conference, confSession) are loaded from server
await conference.removeConfSessions(confSession);

Embedded Objects

Instead of using the relational approach, it is also possible to embed objects within one another. For a general introduction have a look at the related embedded object documentation page.

The SDKs provide methods to work with embedded objects as well such as embedded collections.

Example:
There's a class "Session" and a class "Room". Session has an attribute of the type Room and the attribute is configured as embedded class. Now, if you have an object of the class Session, you can call the method loadRoomAsync(), which will return the embedded object of type Room. In background only the whole parent object (including all embedded objects) gets loaded instead of loading every single Room object by its own. In other words: the Session gets loaded in order for it to contain the latest Room JSON and then the Room gets "extracted" from it.

Limitations

SDKs:
Embedded objects on client side are fully supported in the TypeScript and Swift SDK.
In the C# SQLite and Xamarin SQLite SDK embedded objects are supported partially only. As it is currently only possible to read and post embedded objects in the C# SQLite and Xamarin SQLite SDK - so no delete methods are generated.

Filtering:
It's also not possible to filter the elements of an embedded collection by using a query.

Offline Handling:
Please also note that for the C# & Xamarin SQLite SDK the offline handling using the persistent storage is not working for embedded objects and collections of embedded objects, but using the in-memory cache is already supported.
Create, update and delete actions and full offline handling support will be possible in a future version for the C# SQLite and Xamarin SQLite SDK.

Count Methods:
The static count methods will not include embedded objects or objects included within a collection of embedded objects, as they are not accessible directly anyway.


Example: A Class "Conference" has an embedded reference collection to the class "Session". So one conference has multiple sessions. Let's say there are 2 conferences saved on their own, and 20 sessions saved on their own. The conferences also have 5 sessions embedded. When calling Session.count() or sending a GET request to /count for the Session class, the result will be 20, not 30 (20+2*5). Only these 20 sessions are directly accessible (by their own ID) instead of via their parent (embedding) objects.

Usage

The following examples show how to work with embedded objects:C# (SQLite)

C# (SQLite)
// After loading an existing session you can immediately access any embedded object or collection of embedded objects
Room room = existingSession.Room;
IList<Attendee> attendees = existingSession.Attendees;
 
// Calling the load method on an embedded attribute will cause the embedding session object the be reloaded
Room room = existingSession.LoadRoomAsync().Result;
IList<Attendee> attendees = existingSession.LoadAttendeesAsync().Result;
 
 
// POST
// Room is a single embedded attribute in Session
Room newRoom = new Room();
existingSession.PostRoomAsync(newRoom).Wait();
 
// Attendees is a embedded collection attribute in Session
Attendee newAttendee = new Attendee();
existingSession.PostAttendees( new[] { newAttendee } ).Wait();
 
 
// NOTE: in C# currently it isn't possible to insert any parameter as reference into async methods which stops us to update the embedded object input itself.
// So if we want to work on the current state of the embedded object, we need to reload it from the parent
newAttendee = existingSession.Attendees[0];
 
 
// Update
existingSession.Room.Name = "leipzig"
existingSession.SaveAsync().Wait(); // or existingSession.Room.SaveAsync().Wait(); - both will update the Room embedded object
 
Attendee updateAttendee = existingSession.Attendees[0];
updateAttendee.Firstname = "John";
updateAttendee.SaveAsync();

Objective-C

Objective-C
// After loading an existing session you can immediately access any embedded object or collection of embedded objects
__block Room* room = [existingSession room];
__block Attendees* room = [existingSession attendees];
 
 
// Calling the load, post, delete or save method on an embedded attribute will cause the embedding session object the be reloaded
 
// Load
__block SelfType *blocksafeSelf = self;
[existingSessions loadRoomObjCWithCompletion:^(NSError * error) {
 
if(error == nil)
{
room = blocksafeSelf.existingSession.room;
}
else
{
//something went wrong, handle error
}
}];
 
[existingSession loadAttendeesObjCWithCompletion:^(NSError * error) {
 
if (error == nil) 
{
attendees = blocksafeSelf.existingSession.attendees;
}
else
{
//something went wrong, handle error
}
}];
 
//Post
__block Room *newRoom = [Room new];
[existingSessions postRoomObjC: newRoom completion:^(NSError * error) {
if (error == nil) 
{
room = blocksafeSelf.existingSession.room;
}
else
{
//something went wrong, handle error
}
}];
 
__block Attendee *newAttendee = [Attendee new];
 
[existingSessions.postAttendeesObjC:newAttendee completion:^(NSError * error) {
if (error == nil) 
{
attendees = blocksafeSelf.existingSession.attendees;
}
else
{
//something went wrong, handle error
}
}];
 
//Delete
 
[existingSessions.removeRoomObjCWithCompletion:^(NSError * error) {
if (error == nil) 
{
//successfully deleted
}
else
{
//something went wrong, handle error
}
}];
 
Attendees *removeAttendee = existingSession.attendees.lastObject;
[existingSessions removeAttendeesObjC:removeAttendee completion:^(NSError * error) {
if (error == nil) 
{
//successfully deleted
}
else
{
//something went wrong, handle error
}
}];
 
//Update
 
existingSession.room.name = @"leipzig";
[existingSession.room saveObjCWithLoadAfterwards:true completion:^(NSError * error) {
 
 
if (error == nil) 
{
//successfully updated
else 
{
//something went wrong, handle error
}
}];
 
Attendees *updateAttendee = existingSession.attendees.lastObject;
updateAttendee.firstname = @"John";
[updateAttendee saveObjCWithLoadAfterwards:true completion:^(NSError * error) {
 
 
if (error == nil)
{
//successfully updated
}
else
{
//something went wrong, handle error
}
}];

Swift

Swift
// After loading an existing session you can immediately access any embedded object or collection of embedded objects
let room = existingSession.room;
let attendees = existingSession.attendees;
 
// Calling the load, post, delete or save method on an embedded attribute will cause the embedding session object the be reloaded
 
// Load
 
existingSessions.loadRoom( completion: { (error) in
 
if error == nil {
room = self.existingSession.room
} else {
//something went wrong, handle error
}
})
 
existingSession.loadAttendees( completion: { (error) in
 
if error == nil {
attendees = self.existingSession.attendees
} else {
//something went wrong, handle error
}
})
 
//Post
 
let newRoom = Room()
existingSessions.postRoom( newRoom, completion: { (error) in
if error == nil {
room = self.existingSession.room
} else {
//something went wrong, handle error
}
})
 
let newAttendee = Attendee()
existingSessions.postAttendees( newAttendee, completion: { (error) in
if error == nil {
//successfully deleted
} else {
//something went wrong, handle error
}
})
 
//Delete
 
existingSessions.removeRoom( completion: { (error) in
if error == nil {
//successfully deleted
} else {
//something went wrong, handle error
}
})
 
let removeAttendee = existingSession.attendees.last
existingSessions.removeAttendees( removeAttendee, completion: { (error) in
if error == nil {
attendees = self.existingSession.attendees
}
else {
//something went wrong, handle error
}
})
 
//Update
 
existingSession.room.name = "leipzig"
existingSession.room.save( completion: {(error) in
 
 
if error == nil {
//successfully updated
} else {
//something went wrong, handle error
}
})
 
let updateAttendee = existingSession.attendees.last
updateAttendee.firstname = "John"
updateAttendee.save( completion: {(error) in
 
 
if error == nil {
//successfully updated
} else {
//something went wrong, handle error
}
})


TypeScript

TypeScript
// After loading an existing session you can immediately access any embedded object or collection of embedded objects
const room = existingSession.room;
const attendees = existingSession.attendees;
 
// Calling the load method on an embedded attribute will cause the embedding session object the be reloaded
const room = await existingSessions.loadRoom();
const attendees = await existingSession.loadAttendees();
 
// POST
// room is a single embedded attribute in Session
const newRoom = new Room();
await existingSessions.postRoom(newRoom);
 
// attendees is a embedded collection attribute in Session
const newAttendee = new Attendee();
await existingSessions.postAttendees(newAttendee);
 
// Delete
await existingSessions.removeRoom(newRoom);
await existingSessions.removeAttendees(existingSessions.attendees[0]);
 
// Update
existingSession.room.name = "leipzig"
await existingSession.save(); // or await existingSession.room.save(); - both will update the room embedded object
 
let updateAttendee = existingSession.attendees[0];
updateAttendee.firstname = "John";
await updateAttendee.save();
 
// Files or images
const staticData = new StaticDataTypes();
await existingSession.postStaticData(staticData);
await existingSession.staticData.postFile(testFile);
await existingSession.staticData.deleteFile();


Queries

Our backend supports a lot of query operations. For the full documentation please see here.

But let’s have a look at some examples.

Retrieving a filtered list

To get a list of objects you can set the query parameter.Android

Android
//Load list of conference objects filtered by name
Conference.getConferencesAsync("name like \"Backend\"", new AOMCallback<List<Conference>>() {
@Override
public void isDone(List<Conference> resultObject, boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//Getting list was successful if exception == null
//You can now work with parameter objects which contains loaded elements
}
});

C#

C#
// Load list of conference objects filtered by name
IList<Conference> loadedConferences = await Conference.GetConferencesAsync("name like \"Backend\"");

Objective-C

Objective-C
/* Load list of conference objects filtered by name */
[Conference loadListObjCWithQuerye:@"name like \"Backend\"" completion:^(NSArray<AbstractClientDataModel *> * loadedConfs, NSError * error) {
/* Getting list was successful if exception == null */
/* You can now work with parameter loadedConfs which contains filtered elements */
}];

Swift

Swift
/* Load list of conference objects filtered by name */
Conference.loadList("name like \"backend\"") { conferences, error in
if error == nil {
let loadedConferences = conferences
} else {
//something went wrong, handle errror
}
}

TypeScript

TypeScript
// Load list of conference objects filtered by name
const filteredConferences = await Conference.getConferences('name like "Backend"');


This will load a list of allConference objects, where the name includes the string ’backend’.

Retrieving filtered list of references

If you only want a subset of referenced objects, you can call the “load” method like this:Android

Android
//Assume that our object conference has already referenced confSessions
conference.loadConfSessionsAsync(confSession, new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
conference.getConfSessions();
}
});

C#

C#
// Assume that our object conference already contains referenced confSessions
IList<ConfSession> sessions = await conference.LoadConfSessionAsync("topic=\"C#\"");

Objective-C

Objective-C
/* Assume that our object conference has already referenced confSessions */
[conference loadConfSessionsObjC:@"name like \"Backend\"" completion:^(NSArray<AbstractClientDataModel *> * confSessions, NSError * error) {
NSMutableArray *filteredSessions = confSessions;
}];

Swift

Swift
/* Assume that our object conference has already referenced confSessions */
conference.loadConfSessions("name like \"backend\"") { confSessions, error in
if error == nil {
let loadedConfSessions = confSessions
} else {
//something went wrong, handle errror
}
}

TypeScript

TypeScript
// Assume that our object conference already contains referenced confSessions
const filteredConfSessions = await conference.loadConfSessions('name=="backend"');

Error Handling

The error handling is quite different in the various SDKs, so instead of one common description, the description is in the different SDK tabs:Android

Android

If you use the synchronous methods you have always a try-catch block around the methods which do network requests. If an error occurs an exception of class ApiomatRequestException (which is a subclass of Exception) is thrown and catched. There are several methods to get information about details of this error. The exception object contains specific information like error code and a textual description of the reason.

  • exception.getReason() — Gives you a textual description of the error

  • exception.getExpectedReturnCode() — The expected HTTP return code

  • exception.getReturnCode() — The returned statusCode

  • exception.getStatus() — If is set then this method returns an instance of enum Status


On asynchronous requests isDone method that is executed after request finished contains also an exception object as parameter. If this object does not equal null, an error occurred and it would be nice to handle this. The given object is an instance of the previous explained class ApiomatRequestException. The following snippets gives you an idea how you can handle exception in async methods.

Android
Conference conference = new Conference ();
conference.saveAsync(new AOMEmptyCallback() {
@Override
public void isDone(boolean wasLoadedFromStorage, ApiomatRequestException exception) {
//if exception != null an error occurred
if(exception != null) {
Log.e("ConferenceSave", "An error occurred: " + e.getReason());
}
}
});

C#

C#

All the methods that do network I/O might throw an ApiomatRequestException. In C# there are no checked exceptions like in Java, so you don't have to use try/catch everywhere, but it's advised to do that so you can handle errors when they appear.

There are several Properties to get information about details of an error. The exception object contains specific information like error code and a textual description of the reason.

  • exception.Reason - Gives you a textual description of the error

  • exception.ExpectedReturnCode - The expected HTTP return code

  • exception.ReturnCode - The returned status code

  • exception.Status - Returns an instance of the enum "Status"

The returned status codes are not only HTTP status codes, but also some custom ones. For example the above mentioned error "ID already exists" when trying to sign up a user that has already been signed up before, is the error code 830 and Status.ID_EXISTS in the Status enum.

Here is an example for how to handle exceptions:

C#
Conference conference = new Conference();
try
{
await conference.SaveAsync();
}
catch (ApiomatRequestException e)
{
Console.WriteLine("An error occured during saving a conference: " + e.Reason);
// In Windows Store apps:
// System.Diagnostics.Debug.WriteLine("An error occured during saving a conference: " + e.Reason);
}

In case you want to send another request after the error occurs, be advised that you can't use await in a catch block of an exception in C# 5 (this will probably change in C# 6 with the Roslyn .NET compiler).

If you just want to send the same or other requests asynchronously, initialize a flag with "false" before the try block, set it to "true" in the catch block and send another request if the flag evaluates to true:

C#
Conference conference = new Conference();
bool tryAgain = false;
try
{
await conference.SaveAsync();
}
catch (ApiomatRequestException e)
{
tryAgain = true;
// do what is necessary to fix the problem
// ...
}
if (tryAgain)
{
await CallMoreAsyncCode();
}

However, you might also want to throw the exception so the caller knows about it. If you throw it in the last if-block the exception's stack trace doesn't contain the correct location where the exception happened.

To fix that, you can use the ExceptionDispatchInfo class like in the following example and also omit the flag:

C#
Conference conference = new Conference();
ExceptionDispatchInfo capturedException = null;
try
{
await conference.SaveAsync();
}
catch (ApiomatRequestException e)
{
capturedException = ExceptionDispatchInfo.Capture(e);
}
if (capturedException != null)
{
await CallMoreAsyncCode();
// Throw the exception with the captured info from above
capturedException.Throw();
}

Objective-C

Objective-C

If you use the synchronous methods you have always a try-catch block around the methods which do network requests. If an error occurs an exception of class NSException with name ApiomatRequestExceptionis thrown and catched. There are several methods to get information about details of this error. The exception object contains specific information like error code and a textual description of the reason.

  • [exception name] or exception.name - Gives you the name of the error (should be ApiomatRequestException)

  • [exception reason] or exception.reason- The returned statusCode

  • [[exception userInfo] valueForKey:@"code"] or error?.userInfo["code"] - The returned statusCode (same as above)

  • [[exception userInfo] valueForKey:@"message"] or error?.userInfo["message"] - Gives you a textual description of the error

On asynchronous requests every block that is executed after request finished contains also an NSError object as parameter. If this object not equals nil, an error occurred and it would be nice to handle this. The given error object contains specific information like error code and a textual description of the reason. The following snippets gives you an idea how you can handle exception in async methods.

Objective-C
Conference *conference = [[Conference alloc] init];
[conference saveObjCWithLoadAfterwards:false completion:^(NSError * error) {
/* If error occured the error parameter != NIL */
if(error) 
{
/* errorDomain == com.apiomat.frontend */
NSString *errorDomain = [error domain];
NSInteger errorCode = [error code];
NSString *reason = [[error userInfo] objectForKey:@"message"];
}
}];

The class AOMStatus contains references to every error code (statusCode) that can occur. Also, you can find a textual description of these codes there. You can compare the returned statusCode with a enum of the type AOMStatusCode. The following examples will demonstrate you the usage.

Objective-C
/* get textual description of error code */
NSInteger errorCode = 840;
NSString *errorMsg = [AOMStatus getReasonPhraseForCode:errorCode];

Swift

Swift

The Swift methods perform their work asynchronously and call the passed in completion handler when they are done. If an error occurs an object of Type NSError is passed to the completion handler, otherwise the argument is nil.

There are several methods to get information about details of this error. The error object contains specific information like error code and a textual description of the reason.

  • error?.domain - The error domain (com.apiomat.frontend)

  • error?.userInfo["code"] - The returned statusCode

  • error?.userInfo["message"] - Gives you a textual description of the error

The following snippets gives you an idea how you can handle errors in async methods:

Swift
let conference = Conference()
conference.save { error in
if error == nil {
//successfully saved conference
} else {
/* errorDomain == com.apiomat.frontend */
let errorDomain = error?.domain
let errorCode = error?.userInfo["code"]
let errorMessage = error?.userInfo["message"]
}
}

The class AOMStatus contains references to every error code (statusCode) that can occur. Also, you can find a textual description of these codes there. You can compare the returned statusCode with a enum of the type AOMStatusCode. The following examples will demonstrate you the usage.

Swift
 /* get textual description of error code */
let errorCode = 840
let errorReason = Status.reasonPhrase(forStatusCode: errorCode)

TypeScript

TypeScript

Every request you send will return a Promise. This allows you to use .then() and .catch(e) with those methods, but the more modern way is to use await and use a try-catch block to handle errors. If an error occurs the Promise will be rejected and this rejection will be catched. The exception object contains specific information like error code and a textual description of the reason in the following properties:

  • message: Gives you a textual description of the error

  • statusCode: The returned status code

  • expectedCodes: Returns the HTTP status codes that this request expected

An example will demonstrate the access to an 'error' object:

TypeScript
try {
await conference.loadConfSessions();
} catch (error) {
//Error handling
const errorCode = error.statusCode;
const description = error.message;
}

The class 'AOMStatus' contains references to every statusCode that can occur. Also, you can find a textual description of these codes there. You can compare the returned statusCode with a static property of the type 'AOMStatus'.

TypeScript
try {
await conference.loadConfSessions();
} catch (error) {
//Error handling
if(error.statusCode == AOMStatus.UNAUTHORIZED) {
//Handling this error
}
}

Custom Error Codes

ApiOmat supports to define and throw your own error codes within e.g. a Native Module. Those custom error codes will be delegated through each SDK to allow you to catch and handle them within your projects.

Error Code Limitation

The SDKs internal basic libraries often block HTTP responses with unsupported error codes or throw ProtocolExceptions. To avoid such complications, since this Apiomat version ( 19.11 - Yambas 3.4 ) the custom error codes are no longer being returned directly as HTTP response code. Instead the custom error code is now added to the newly introduced error response JSON while the actual HTTP response code will be of the HTTP/1.1 RFC compliant ones. All SDKs were updated so that the new error responses can be mapped to the usual SDK internal error format you're already used to.


Cache and Offline Handling

When using the Objective-C SDK, please note:

The class AOMModelStore inherits from the class NSURLCache. So if you have any problems with clear cache functions in the simulator especially for ios 9, you should use the storagePolicy: NSURLCacheStorageAllowedInMemoryOnly.

For info about how to use caching and offline data, please look here: Offline Handling in SDKs

Meta data of data model

To access information about the structure of your app, you can use the appConfig, that provides the names of all available modules including it's models and attributes in JSON format.

For example:appConfig.json

appConfig.json
{
"app": {
"name": "Conference",
"modules": [
{
"module": {
"name": "Basics",
"type": "STATIC",
"usedInSystem": "TEST",
"classes": [
{
"class": {
"name": "User",
"attributes": [
{
"attribute": {
"name": "userName",
"type": "String"
}
},
...snip other attributes
}
},
... snip other classes
}
},
... snip other modules
}
}



TypeScript

TypeScript
const appConfig = User.APPCONFIG;
//now you can access the name of the app for example
const appName = appConfig.app.name;

Adding and modifying headers

It is possible to add new or modify existing headers sent with each SDK request to ApiOmat using a special method in the Datastore class. Like the Datastore.configure..() method, setting additional request headers this way will persist until another call to the method is executed:Android

Android
Map<String, String> myHeaders = new HashMap<>();
myHeaders.put("cookie", "123456");
 
/* adding multiple header */
Datastore.setCustomHeaders(myHeaders);
 
/* removing all headers */
Datastore.setCustomHeaders(null);

C#

C#
/* Add custom headers */
Datastore.Instance.CustomHeaders = new Dictionary<string, string> () { {"foo", "bar"} };
 
/* Remove custom headers */
Datastore.Instance.CustomHeaders = null;

Objective-C

Objective-C
/* setting a custom header */
NSMutableDictionary *customHeaders = [[NSMutableDictionary alloc] initWithDictionary: [[DataStore sharedInstance] getCustomHeaders]];
[customHeaders setObject:@"Header_Value" forKey:@"Header_Name"];
[[DataStore sharedInstance] setCustomHeaders:customHeaders];
 
/* removing a custom header */
NSMutableDictionary *customHeaders = [[NSMutableDictionary alloc] initWithDictionary: [[DataStore sharedInstance] getCustomHeaders]];
[customHeaders removeObjectForKey:@"Header_Name"];
[[DataStore sharedInstance] setCustomHeaders:customHeaders];
 
/* adding multiple headers */
NSDictionary* headers = @{@"Header_Name1" : @"Header_Value1",
@"Header_Name2" : @"Header_Value2" };
[[DataStore sharedInstance] setCustomHeaders:headers];
 
/* removing all headers */
NSMutableDictionary *customHeaders = [[NSMutableDictionary alloc] initWithDictionary: [[DataStore sharedInstance] getCustomHeaders]];
[customHeaders removeAllObjects];
[[DataStore sharedInstance] setCustomHeaders:customHeaders];

Swift

Swift
/* setting a custom header */
DataStore.sharedInstance.setCustomHeaders(["cookie", "123456"])
 
/* setting multiple headers */
DataStore.sharedInstance.setCustomHeaders(["cookie", "123456", "key", "value"])
 
/* removing all headers */
DataStore.sharedInstance.setCustomHeaders(nil)

TypeScript

TypeScript
/* setting a custom header */
Datastore.Instance.customHeaders = {"foo": "bar"}
 
/* setting multiple headers */
Datastore.Instance.customHeaders = {
"foo": "bar",
"key": "value"
};
 
/* removing all headers */
Datastore.Instance.customHeaders = undefined;

Configuring Bouncer auth

If the authentication service Bouncer is installed and you want to use the Bouncer token authentication in your SDK, there are configuration settings in the SDK.
The SDK also provides a method in the Datastore, which configures the SDK to send requests with the auth token from the Bouncer.
You can also see an extended configuration example in the Quickstart guides of the specific SDK.Android

Android

To communicate with the authentication service Bouncer, we have added support for the AppAuth library.

Here we show you how you use the ApiOmat Android SDK with the library. See AppAuth documentation to get more information about implementing the authorization code flow.

First of all, add the following dependency to your build.gradle.

implementation 'net.openid:appauth:0.10.0'

AppAuth uses androidX libraries and requires Android API 16 and above. The minimum supported Gradle version is 6.7.1.

To receive the authorization callback define following activity in your manifest file. You don't need to implement this activity. This activity will act like a proxy for your authorization request.

<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.example"/> <!-- Redirect URI scheme -->
</intent-filter>
</activity>

Configure your datastore with authorization service to perform requests with bearer token. The application Context is needed to let the datastore configure the AppAuthStateManager, which is responsible to persist the current authorization state.

// set the Context to the datastore to enable Bouncer auth in the SDK
Datastore.configureWithAuthService( context );

With the generated SdkInfo you get the realm name and the URL of Bouncer.

// use SdkInfo.AUTHSERVICE_REALMNAME to get the realm name
final String yourRealm = SdkInfo.AUTHSERVICE_REALMNAME
 
// use SdkInfo.AUTHSERVICE_URL to get the url to Bouncer
final String yourAuthServiceUrl = SdkInfo.AUTHSERVICE_URL

The AppAuthStateManager provide the following methods for the application developer to manage the authorization state. The ApiOmat client uses the AppAuthStateManager to get the access token. It is mandatory that you use those methods to ensure that the state is the latest.

AuthState updateAfterAuthorization(AuthorizationResponse response, AuthorizationException ex);
AuthState updateAfterTokenResponse(TokenResponse response, AuthorizationException ex);
AuthState updateAfterRegistration(RegistrationResponse response, AuthorizationException ex);

As an example: use updateAfterTokenResponse after you exchanged the authorization code for a token. The following methods retrieves the token and adds it to AppAuthStateManager.

private void updateTokenInAppAuthStateManager( final AuthorizationResponse authResponse )
{
final AuthorizationService service = new AuthorizationService( this );
 
// Creates a follow-up request to exchange a received authorization code for tokens.
final TokenRequest tokenRequest = authResponse.createTokenExchangeRequest( );
 
// Sends a request to the authorization service to exchange a code granted as part of an authorization request
// for a token. The result of this request will be sent to the provided callback handler.
service.performTokenRequest( tokenRequest, new ClientSecretBasic( "MyClient" ),
( tokenResponse, tokenException ) ->
// update the session with the current token
AppAuthStateManager.getInstance( getApplicationContext( ) ).updateAfterTokenResponse( tokenResponse, tokenException );
}

Use performActionWithFreshTokens to execute the API call with a fresh access token. It will automatically ensure that the tokens hold by the AuthState in the AppAuthStateManager used by that ApiOmat client are fresh and refresh them when needed.

AuthorizationService service = new AuthorizationService(this);
AppAuthStateManager.getInstance(this).getCurrent().getCurrentAuth().performActionWithFreshTokens(service, new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException authException) {
// Handle token refresh error here especially because an authorisation error means that the client have to authenticate again and be redirected to the auth activity
// execute your requests here
}
});

TypeScript

TypeScript
// use the configuration values from SdkInfo.ts: AUTHSERVICE_REALMNAME and AUTHSERVICE_URL to configure the keycloak-js instance
const kc = Keycloak({realm: SdkInfo.AUTHSERVICE_REALMNAME, clientId: 'testclient', url: SdkInfo.AUTHSERVICE_URL})
 
 
// set the keycloak-js instance to the datastore to enable the Bouncer auth in the SDK
Datastore.configureWithAuthService(kc as AuthServiceInstance);

Using ApiOmat Analytics

If ApiOmat Analytics is activated, the analytics module is automatically added to the App-Backend. The analytics URL and app key will be generated into the User class of the SDK.Android

Android

Add the following dependency to your build.gradle

implementation 'net.openid:appauth:0.10.0'

AppAuth uses androidX libraries and requires Android API 16 and above. The minimum supported Gradle version is 6.7.1.

To receive the authorization callback define following activity in your manifest file. You don't need to implement this activity. This activity will act like a proxy for your authorization request.

<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.example"/> <!-- Redirect URI scheme -->
</intent-filter>
</activity>

Configure your datastore with authorization flow to perform requests with bearer token. The application Context is needed to let the datastore configure the AppAuthStateManager, which is responsible to persist the current authorization state.

// set the Context to the datastore to enable Bouncer auth in the SDK
Datastore.configureWithAuthorizationFlow( context );

With the generated SdkInfo you get the realm name and the URL of Bouncer.

// use SdkInfo.AUTHSERVICE_REALMNAME to get the realm name
final String yourRealm = SdkInfo.AUTHSERVICE_REALMNAME
 
// use SdkInfo.AUTHSERVICE_URL to get the url to Bouncer
final String yourAuthServiceUrl = SdkInfo.AUTHSERVICE_URL

The AppAuthStateManager provide the following methods for the application developer to manage the authorization state. The ApiOmat client uses the AppAuthStateManager to get the access token. It is mandatory that you use those methods to ensure that the state is the latest.

AuthState updateAfterAuthorization( AuthorizationResponse response, AuthorizationException ex);
AuthState updateAfterTokenResponse(TokenResponse response, AuthorizationException ex);
AuthState updateAfterRegistration(RegistrationResponse response, AuthorizationException ex);

As an example: use updateAfterTokenResponse after you exchanged the authorization code for a token.

private void retrieveTokens( final AuthorizationResponse authResponse )
{
final AuthorizationService service = new AuthorizationService( this );
 
// Creates a follow-up request to exchange a received authorization code for tokens.
final TokenRequest tokenRequest = authResponse.createTokenExchangeRequest( );
 
// Sends a request to the authorization service to exchange a code granted as part of an authorization request
// for a token. The result of this request will be sent to the provided callback handler.
service.performTokenRequest( tokenRequest, new ClientSecretBasic( "MyClient" ),
( tokenResponse, tokenException ) ->
// update the session with the current token
AppAuthStateManager.getInstance( getApplicationContext( ) ).updateAfterTokenResponse( tokenResponse, tokenException );
}

Use performActionWithFreshTokens to execute the API call with a fresh access token. It will automatically ensure that the tokens hold by the AuthState in the AppAuthStateManager used by that ApiOmat client are fresh and refresh them when needed.

AuthorizationService service = new AuthorizationService(this);
AppAuthStateManager.getInstance(this).getCurrent().getCurrentAuth().performActionWithFreshTokens(service, clientAuth, new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException authException) {
// Handle token refresh error here especially because an authorisation error means that the client have to authenticate again and be redirected to the auth activity
// execute your requests here
}
});

See AppAuth documentation to get more information about implementing the authorization code flow

C#

C#
  • in order to use ApiOmat Analytics functionality within the C# SDK you need to include the Countly dependency via NuGet from https://www.nuget.org/packages?q=Countly

  • if ApiOmat Analytics is installed and fully configured on server side the AnalyticsHost and AnalyticsApiKey will be generated into the User.cs class

  • Unfortunatly the native Xamarin Countly SDK still contains old PCL Storage dependencies, which may conflict with ApiOmat's newer dependencies. That is the reason why we cannot directly deliver the Countly SDK within our C# SDK.

  • There are special Countly Xamarin SDK dependencies for IOS and Android. Feel free to include them into your NuGet Manager to use Countly's functions to access ApiOmat Analytics.

Objective-C

Objective-C

Import Countly.h in your application delegate and inside "application:didFinishLaunchingWithOptions.

// The analytics host url and the app key are saved in the AOMUser class in your ApiOmat SDK.
 
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
CountlyConfig *config = [CountlyConfig new];
[config setAppKey: analyticsAppKey];
[config setHost: analyticsHost];
[[Countly sharedInstance] startWithConfig:config];
}
 
// send a custom event
[[Countly sharedInstance] recordEvent:"custom_event"];

TypeScript

TypeScript

There is a file README_Analytics.md in the com folder of the SDK, which contains an installation guide for the analytics dependency. The installation also depends on the environment in which the SDK is used (Node, Web, Phonegap, ..).

The following code snippet shows the usage of ApiOmat Analytics ("Countly").

// The analytics URL and the app key are saved in the User class in your ApiOmat SDK.
Countly.init({
app_key: User.ANALYTICSAPPKEY,
url: User.ANALYTICSURL
});
 
//start a session
Countly.track_sessions();
 
 
// send a custom event
Countly.add_event({ key:"buttonClick" });