horus-sync-client-kmm

Facilitates local data storage and synchronization with a remote server, ensuring data security and integrity. Supports file uploads, entity restrictions, and provides an intuitive interface.

Android JVMKotlin/Native
GitHub stars1
Authorsapptanksas
Open issues0
LicenseGNU Affero General Public License v3.0
Creation dateover 1 year ago

Last activity2 months ago
Latest release0.18.0 (16 days ago)

Horusync Logo
Build Status Latest Stable Version License Ask DeepWiki

Please note: This library currently is testing stage until publish the version 1.0.0. Meanwhile, it could have breaking changes in the API.

Table of Contents

Horusync client KMM

Horus is a client library for Kotlin Multiplatform aimed at providing an easy and simple way to store data locally and synchronize it with a remote server, ensuring data security and integrity.

Use Horus in server side to synchronize the data with the clients.

Features

  • Easy-to-use interface.
  • It is safety.
  • Validates data integrity across clients.
  • Support for file uploads.
  • Support for entity restrictions.

1. How to start

Install

Gradle

Add the repository and dependency in your build.gradle file:

kotlin {
     
     /** ... Another configurations ... */
     
     sourceSets {
          androidMain.dependencies {
              implementation("org.apptank.horus:client-android:{version}") // Android
          }
          commonMain.dependencies {
            /** ... Dependencies for common module ... */
          }
          iosMain.dependencies {
              implementation("org.apptank.horus:client:{version}") // IOS
          }
    }
}

Android

Setup

Permissions

Horusync needs the INTERNET and ACCESS_NETWORK_STATE permissions to be implemented in the AndroidManifest.xml of your application.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Initialization

In the Application of your app configure horus using the HorusConfigurator class passing a HorusConfig object with the base server URL and the configuration of the pending actions.

The UploadFilesConfig class defines the settings for uploading files to the server.

  • baseStoragePath: The base path where the files will be stored.
  • mimeTypesAllowed: List of allowed mime types.
  • maxFileSize: Maximum file size allowed in bytes.

The PushPendingActionsConfig class defines the settings for managing pending actions before synchronization. It includes the size of batches and the time expiration threshold to do synchronization.

  • batchSize: Number of actions to synchronize in each batch.
  • expirationTime: The maximum time in seconds allowed between synchronizations before forcing one. Default is 12 hours.

It is also necessary to register HorusActivityLifeCycle to listen to the application's life cycle.

class MainApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    setupHorus()
  }

  private fun setupHorus() {

    val BASE_SERVER_URL = "https://api.yourdomain.com/sync"

    val uploadFileConfig = UploadFilesConfig(
      baseStoragePath = filesDir.absolutePath,
      mimeTypesAllowed = listOf(FileMimeType.IMAGE_JPEG_IMAGE_JPG, FileMimeType.IMAGE_PORTABLE_NETWORK_GRAPHICS),
      maxFileSize = 1024 * 1024 * 5 // 5MB
    )

    // Configure Horus      
    val config = HorusConfig(
      BASE_SERVER_URL,
      uploadFileConfig,
      PushPendingActionsConfig(batchSize = 10, expirationTime = 60 * 60 * 12L),
      mapOf("custom-header" to "custom-value"),
      isDebug = true
    )

    HorusConfigurator(config).configure(this)

    // Register the activity lifecycle callbacks      
    registerActivityLifecycleCallbacks(HorusActivityLifeCycle())
  }
}   

IOS

Setup

  • Add the configuration of -lsqlite3 in the application's linker flags in XCode > Build Settings > "Other Linker Flags".

Initialization

1. Create an app delegate to handle lifecycle events

    class AppDelegate: NSObject, UIApplicationDelegate {  
	    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  
	        IOSHorusLifeCycle().onCreate()  
	        IOSHorusLifeCycle().onResume()  
	        return true  
	    }  
	  
	    func applicationDidBecomeActive(_ application: UIApplication) {  
	        IOSHorusLifeCycle().onResume()  
	    }  
	  
	    func applicationWillResignActive(_ application: UIApplication) {  
	        IOSHorusLifeCycle().onPause()  
	    }
}

2. Implement a NetworkValidator to check the network status

final class NetworkValidator: ClientINetworkValidator {
    
    static let shared = NetworkValidator()

    private let queue = DispatchQueue(label: "NetworkMonitor")
    private let mutableQueue = DispatchQueue(label: "NetworkMonitor.mutable")
    private let monitor = NWPathMonitor()
    private var networkChangeCallback: (() -> Void)?
    private var isMonitoring = false
    
    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self = self else { return }

            if self.isMonitoring {
                self.networkChangeCallback?()
            }
        }
        monitor.start(queue: queue)
    }

    func isNetworkAvailable() -> Bool {
        let path = monitor.currentPath
        return path.status == .satisfied
    }

    func onNetworkChange(callback: @escaping () -> Void) {
        self.networkChangeCallback = callback
        callback()
    }

    func registerNetworkCallback() {
        guard !isMonitoring else { return }
        setIsMonitoring(true)
    }

    func unregisterNetworkCallback() {
        guard isMonitoring else { return }
        setIsMonitoring(false)
    }
    
    private func setIsMonitoring(_ bool: Bool) {
        mutableQueue.sync {
            isMonitoring = bool
        }
    }
}

3. Configure Horus in the initialization of the application

@main
struct iOSApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    init(){
        IOSHorusConfigurator().configure(networkValidator: NetworkValidator.shared)
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

2. How to use

The main way to interact with Horus is through its Facade class called HorusDataFacade, with it you will manage all data operations of your application.

Callbacks

Initialization validation

Horus requires an internal validation and check before starting to be used, to ensure that Horus is ready to operate, use the onReady method of the HorusDataFacade class to know when this happens.

HorusDataFacade.onReady {
  /** PUT YOUR CODE **/
}  

Subscribe to data changes

To know when a record is inserted, updated or deleted in an entity, subscribe to data changes by adding a DataChangeListener using the addDataChangeListener method of the **HorusDataFacade ** class.

HorusDataFacade.addDataChangeListener(object : DataChangeListener {

  override fun onInsert(entity: String, id: String, data: DataMap) {
    /** WHEN IS INSERTED A NEW RECORD **/
  }
  override fun onUpdate(entity: String, id: String, data: DataMap) {
    /** WHEN IS UPDATED A RECORD **/
  }

  override fun onDelete(entity: String, id: String) {
    /** WHEN IS DELETED A RECORD **/
  }
})  

Remove the listener

HorusDataFacade.removeDataChangeListener(listener)

Clear all listeners

HorusDataFacade.removeAllDataChangeListeners()

Data management

Horus internally validates whether there is an internet connection or not to synchronize the information with the server. The operations do not depend on an internet connection, Horus will always first register in the local database of the device.

  • The ID generated internally by Horus for the records of each entity is in UUID format.
  • Currently has not support for integer IDs.

Insert data into an entity

To add a new record, use the insert method passing the entity name and a map with the record attributes. The method will return a DataResult with the new record ID if the operation was successful.

Attention

  • Horus always generates the record ID internally, so it should not be given as a parameter within the attributes.
val entityName = "users"
val newData = mapOf("name":"Aston", "lastname":"Coleman")

val result = HorusDataFacade.insert(entityName, newData)

when (result) {
  is DataResult.Success -> {
    val entityId = result.data
    /** YOUR CODE HERE WHEN INSERT IS SUCCESSFUL */
  }
  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN INSERT FAILS */
  }
  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN INSERT FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Alternative result validation

result.fold(
  onSuccess = {
    val entityId = result.data
    /** YOUR CODE HERE WHEN INSERT IS SUCCESSFUL */
  },
  onFailure = {
    /** YOUR CODE HERE WHEN INSERT FAILS */
  })  

Update data of a record

To update a record, use the update method passing the entity name, the record ID, and a map with the attributes to update.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"
val newName = "Elton"
val result = HorusDataFacade.update(
  "users", userId, mapOf(
    "name" to newName
  )
)
when (result) {
  is DataResult.Success -> {
    /** YOUR CODE HERE WHEN SUCCESS */
  }

  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN FAILURE */
  }

  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN UPDATE FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Delete a record

To delete a record, use the delete method passing the entity name and the record ID.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"

val result = HorusDataFacade.delete("users", userId)

when (result) {
  is DataResult.Success -> {
    /** YOUR CODE HERE WHEN SUCCESS */
  }
  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN FAILURE */
  }
  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN UPDATE FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Simple record query

To query the records of an entity, use the querySimple method passing the entity name and a list of search conditions.

Optional parameters:

  • orderBy: Name of the column by which the query will be ordered.
  • limit: Limit of records to obtain.
  • offset: Number of records to skip.
val whereConditions = listOf(
  SQL.WhereCondition(SQL.ColumnValue("age", 10), SQL.Comparator.GREATER_THAN_OR_EQUALS),
)
HorusDataFacade.querySimple("users", whereConditions, orderBy = "name")
  .fold(
    onSuccess = { users ->
      //** YOUR CODE HERE WHEN SUCCESS */ 
    },
    onFailure = {
      //** YOUR CODE HERE WHEN FAILURE */ 
    })

Complex query

To do a query with more complex conditions, use the query method passing a query builder object.

val builder = SimpleQueryBuilder("entity").where(
  SQL.WhereCondition(SQL.ColumnValue("name", "John%"), SQL.Comparator.LIKE)
).whereOr(
  SQL.WhereCondition(SQL.ColumnValue("lastname","John%"), SQL.Comparator.LIKE)
)

HorusDataFacade.query(builder)

Query by geographic coordinates (Nearby search)

To find records within a certain distance from a geographic point, use the SQL.Coordinates.WithIn extension. This is useful for location-based queries, such as finding nearby places or points of interest.

The coordinates must be stored in the database as a string with the format "latitude,longitude" (e.g., "4.6097,-74.0817").

Parameters:

  • column: The name of the column containing the coordinates
  • point: The reference point (Horus.Point) containing latitude and longitude.
  • distanceInKm: The maximum distance in kilometers from the reference point.
// Reference point (e.g., user's current location)
val currentLocation = Horus.Point(
    latitude = 4.6097,
    longitude = -74.0817
)

// Find all places within 5 km from the current location
val builder = SimpleQueryBuilder("places").withExtension(
    SQL.Coordinates.WithIn(
        column = "location",
        point = currentLocation,
        distanceInKm = 5.0
    )
)

HorusDataFacade.query(builder).fold(
    onSuccess = { nearbyPlaces ->
        // Process the nearby places
        nearbyPlaces.forEach { place ->
            println("Found: ${place["name"]}")
        }
    },
    onFailure = { error ->
        // Handle error
    }
)

Note: The coordinate extension uses a bounding box approximation for efficient filtering. This works well for distances up to several hundred kilometers.

Get a record by ID

To get a record by its ID, use the getById method passing the entity name and the record ID.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"

val user = HorusDataFacade.getById("users", userId)

if (user != null) {
//** YOUR CODE HERE WHEN RECORD EXISTS **/
}

Get count records in a entity

To get the number of records in an entity, use the countRecordFromEntity method passing the entity name.

HorusDataFacade.countRecordFromEntity("users").fold(
  onSuccess = { count ->
    //** YOUR CODE HERE WHEN SUCCESS */
  },
  onFailure = {
    //** YOUR CODE HERE WHEN FAILURE */
  }
)

Query Data shared

To query data shared by another user, use the queryShared method passing the entity name, the entity ID and a list of attributes to filter the data.

HorusDataFacade.queryDataShared(
  entityName,
  entityId,
  listOf(Horus.Attribute("attr", "value"))
)

Upload files

To upload files to the server is simple, use the uploadFile method passing the file data in bytes and then use the getFileUrl method to get the file URL to use where you need it.

val fileReference = HorusDataFacade.uploadFile(fileData)

val fileUrl = HorusDataFacade.getFileUrl(fileReference)

Utilities

Get entities name

To get the list of entities that are being managed by Horus, use the getEntityNames method.

val entityNames = HorusDataFacade.getEntityNames()

Force synchronization

To force the synchronization of the data with the server, use the forceSync method.

HorusDataFacade.forceSync(onSuccess = {
  /** YOUR CODE HERE WHEN SUCCESS */
}, onFailure = {
  /** YOUR CODE HERE WHEN FAILURE */
})

Validate if exists data to synchronize

To validate if there is data to synchronize, use the hasDataToSync method.

val hasDataToSync = HorusDataFacade.hasDataToSync()

Get the last synchronization date

To get the last synchronization date, use the getLastSyncDate method. This method will return a timestamp in seconds.

val lastSyncDate = HorusDataFacade.getLastSyncDate()

Authentication

Horus needs a user access token in session to be able to send the information to the remote server, for this use the HorusAuthentication class within the life cycle of the user session of your application to be able to configure the access token.

Setup access token

HorusAuthentication.setupUserAccessToken("{ACCESS_TOKEN}")  

Clear session

HorusAuthentication.clearSession()  

Setup to act as a guest user by another user

If the user must act on behalf of another user by invitation, the owner user of the entities must be configured as follows:

HorusAuthentication.setUserActingAs("{USER_OWNER_ID}")  

Entity restrictions

You can add specific restrictions for an entity, for example, to limit the number of records that can be stored in an entity. If the restriction is reached, the operation will fail and data result will be DataResult.NotAuthorized.

Use the setEntityRestrictions method of the HorusDataFacade class to set the restrictions.

 HorusDataFacade.setEntityRestrictions(
  listOf(
    MaxCountEntityRestriction("tasks", 100) // Limit to 100 records in the "tasks" entity
  )
)

Restrictions supported

  • MaxCountEntityRestriction: Limit the number of records that can be stored in an entity.

3. Local testing

Publish locally

../gradlew publishToMavenLocal

Location of the dependencies

C:\Users[User].m2\repository\com\apptank\horus\client\core

Android JVMKotlin/Native
GitHub stars1
Authorsapptanksas
Open issues0
LicenseGNU Affero General Public License v3.0
Creation dateover 1 year ago

Last activity2 months ago
Latest release0.18.0 (16 days ago)

Horusync Logo
Build Status Latest Stable Version License Ask DeepWiki

Please note: This library currently is testing stage until publish the version 1.0.0. Meanwhile, it could have breaking changes in the API.

Table of Contents

Horusync client KMM

Horus is a client library for Kotlin Multiplatform aimed at providing an easy and simple way to store data locally and synchronize it with a remote server, ensuring data security and integrity.

Use Horus in server side to synchronize the data with the clients.

Features

  • Easy-to-use interface.
  • It is safety.
  • Validates data integrity across clients.
  • Support for file uploads.
  • Support for entity restrictions.

1. How to start

Install

Gradle

Add the repository and dependency in your build.gradle file:

kotlin {
     
     /** ... Another configurations ... */
     
     sourceSets {
          androidMain.dependencies {
              implementation("org.apptank.horus:client-android:{version}") // Android
          }
          commonMain.dependencies {
            /** ... Dependencies for common module ... */
          }
          iosMain.dependencies {
              implementation("org.apptank.horus:client:{version}") // IOS
          }
    }
}

Android

Setup

Permissions

Horusync needs the INTERNET and ACCESS_NETWORK_STATE permissions to be implemented in the AndroidManifest.xml of your application.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Initialization

In the Application of your app configure horus using the HorusConfigurator class passing a HorusConfig object with the base server URL and the configuration of the pending actions.

The UploadFilesConfig class defines the settings for uploading files to the server.

  • baseStoragePath: The base path where the files will be stored.
  • mimeTypesAllowed: List of allowed mime types.
  • maxFileSize: Maximum file size allowed in bytes.

The PushPendingActionsConfig class defines the settings for managing pending actions before synchronization. It includes the size of batches and the time expiration threshold to do synchronization.

  • batchSize: Number of actions to synchronize in each batch.
  • expirationTime: The maximum time in seconds allowed between synchronizations before forcing one. Default is 12 hours.

It is also necessary to register HorusActivityLifeCycle to listen to the application's life cycle.

class MainApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    setupHorus()
  }

  private fun setupHorus() {

    val BASE_SERVER_URL = "https://api.yourdomain.com/sync"

    val uploadFileConfig = UploadFilesConfig(
      baseStoragePath = filesDir.absolutePath,
      mimeTypesAllowed = listOf(FileMimeType.IMAGE_JPEG_IMAGE_JPG, FileMimeType.IMAGE_PORTABLE_NETWORK_GRAPHICS),
      maxFileSize = 1024 * 1024 * 5 // 5MB
    )

    // Configure Horus      
    val config = HorusConfig(
      BASE_SERVER_URL,
      uploadFileConfig,
      PushPendingActionsConfig(batchSize = 10, expirationTime = 60 * 60 * 12L),
      mapOf("custom-header" to "custom-value"),
      isDebug = true
    )

    HorusConfigurator(config).configure(this)

    // Register the activity lifecycle callbacks      
    registerActivityLifecycleCallbacks(HorusActivityLifeCycle())
  }
}   

IOS

Setup

  • Add the configuration of -lsqlite3 in the application's linker flags in XCode > Build Settings > "Other Linker Flags".

Initialization

1. Create an app delegate to handle lifecycle events

    class AppDelegate: NSObject, UIApplicationDelegate {  
	    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {  
	        IOSHorusLifeCycle().onCreate()  
	        IOSHorusLifeCycle().onResume()  
	        return true  
	    }  
	  
	    func applicationDidBecomeActive(_ application: UIApplication) {  
	        IOSHorusLifeCycle().onResume()  
	    }  
	  
	    func applicationWillResignActive(_ application: UIApplication) {  
	        IOSHorusLifeCycle().onPause()  
	    }
}

2. Implement a NetworkValidator to check the network status

final class NetworkValidator: ClientINetworkValidator {
    
    static let shared = NetworkValidator()

    private let queue = DispatchQueue(label: "NetworkMonitor")
    private let mutableQueue = DispatchQueue(label: "NetworkMonitor.mutable")
    private let monitor = NWPathMonitor()
    private var networkChangeCallback: (() -> Void)?
    private var isMonitoring = false
    
    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self = self else { return }

            if self.isMonitoring {
                self.networkChangeCallback?()
            }
        }
        monitor.start(queue: queue)
    }

    func isNetworkAvailable() -> Bool {
        let path = monitor.currentPath
        return path.status == .satisfied
    }

    func onNetworkChange(callback: @escaping () -> Void) {
        self.networkChangeCallback = callback
        callback()
    }

    func registerNetworkCallback() {
        guard !isMonitoring else { return }
        setIsMonitoring(true)
    }

    func unregisterNetworkCallback() {
        guard isMonitoring else { return }
        setIsMonitoring(false)
    }
    
    private func setIsMonitoring(_ bool: Bool) {
        mutableQueue.sync {
            isMonitoring = bool
        }
    }
}

3. Configure Horus in the initialization of the application

@main
struct iOSApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    init(){
        IOSHorusConfigurator().configure(networkValidator: NetworkValidator.shared)
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

2. How to use

The main way to interact with Horus is through its Facade class called HorusDataFacade, with it you will manage all data operations of your application.

Callbacks

Initialization validation

Horus requires an internal validation and check before starting to be used, to ensure that Horus is ready to operate, use the onReady method of the HorusDataFacade class to know when this happens.

HorusDataFacade.onReady {
  /** PUT YOUR CODE **/
}  

Subscribe to data changes

To know when a record is inserted, updated or deleted in an entity, subscribe to data changes by adding a DataChangeListener using the addDataChangeListener method of the **HorusDataFacade ** class.

HorusDataFacade.addDataChangeListener(object : DataChangeListener {

  override fun onInsert(entity: String, id: String, data: DataMap) {
    /** WHEN IS INSERTED A NEW RECORD **/
  }
  override fun onUpdate(entity: String, id: String, data: DataMap) {
    /** WHEN IS UPDATED A RECORD **/
  }

  override fun onDelete(entity: String, id: String) {
    /** WHEN IS DELETED A RECORD **/
  }
})  

Remove the listener

HorusDataFacade.removeDataChangeListener(listener)

Clear all listeners

HorusDataFacade.removeAllDataChangeListeners()

Data management

Horus internally validates whether there is an internet connection or not to synchronize the information with the server. The operations do not depend on an internet connection, Horus will always first register in the local database of the device.

  • The ID generated internally by Horus for the records of each entity is in UUID format.
  • Currently has not support for integer IDs.

Insert data into an entity

To add a new record, use the insert method passing the entity name and a map with the record attributes. The method will return a DataResult with the new record ID if the operation was successful.

Attention

  • Horus always generates the record ID internally, so it should not be given as a parameter within the attributes.
val entityName = "users"
val newData = mapOf("name":"Aston", "lastname":"Coleman")

val result = HorusDataFacade.insert(entityName, newData)

when (result) {
  is DataResult.Success -> {
    val entityId = result.data
    /** YOUR CODE HERE WHEN INSERT IS SUCCESSFUL */
  }
  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN INSERT FAILS */
  }
  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN INSERT FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Alternative result validation

result.fold(
  onSuccess = {
    val entityId = result.data
    /** YOUR CODE HERE WHEN INSERT IS SUCCESSFUL */
  },
  onFailure = {
    /** YOUR CODE HERE WHEN INSERT FAILS */
  })  

Update data of a record

To update a record, use the update method passing the entity name, the record ID, and a map with the attributes to update.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"
val newName = "Elton"
val result = HorusDataFacade.update(
  "users", userId, mapOf(
    "name" to newName
  )
)
when (result) {
  is DataResult.Success -> {
    /** YOUR CODE HERE WHEN SUCCESS */
  }

  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN FAILURE */
  }

  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN UPDATE FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Delete a record

To delete a record, use the delete method passing the entity name and the record ID.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"

val result = HorusDataFacade.delete("users", userId)

when (result) {
  is DataResult.Success -> {
    /** YOUR CODE HERE WHEN SUCCESS */
  }
  is DataResult.Failure -> {
    /** YOUR CODE HERE WHEN FAILURE */
  }
  is DataResult.NotAuthorized -> {
    /** YOUR CODE HERE WHEN UPDATE FAILS BECAUSE OF NOT AUTHORIZED */
  }
}  

Simple record query

To query the records of an entity, use the querySimple method passing the entity name and a list of search conditions.

Optional parameters:

  • orderBy: Name of the column by which the query will be ordered.
  • limit: Limit of records to obtain.
  • offset: Number of records to skip.
val whereConditions = listOf(
  SQL.WhereCondition(SQL.ColumnValue("age", 10), SQL.Comparator.GREATER_THAN_OR_EQUALS),
)
HorusDataFacade.querySimple("users", whereConditions, orderBy = "name")
  .fold(
    onSuccess = { users ->
      //** YOUR CODE HERE WHEN SUCCESS */ 
    },
    onFailure = {
      //** YOUR CODE HERE WHEN FAILURE */ 
    })

Complex query

To do a query with more complex conditions, use the query method passing a query builder object.

val builder = SimpleQueryBuilder("entity").where(
  SQL.WhereCondition(SQL.ColumnValue("name", "John%"), SQL.Comparator.LIKE)
).whereOr(
  SQL.WhereCondition(SQL.ColumnValue("lastname","John%"), SQL.Comparator.LIKE)
)

HorusDataFacade.query(builder)

Query by geographic coordinates (Nearby search)

To find records within a certain distance from a geographic point, use the SQL.Coordinates.WithIn extension. This is useful for location-based queries, such as finding nearby places or points of interest.

The coordinates must be stored in the database as a string with the format "latitude,longitude" (e.g., "4.6097,-74.0817").

Parameters:

  • column: The name of the column containing the coordinates
  • point: The reference point (Horus.Point) containing latitude and longitude.
  • distanceInKm: The maximum distance in kilometers from the reference point.
// Reference point (e.g., user's current location)
val currentLocation = Horus.Point(
    latitude = 4.6097,
    longitude = -74.0817
)

// Find all places within 5 km from the current location
val builder = SimpleQueryBuilder("places").withExtension(
    SQL.Coordinates.WithIn(
        column = "location",
        point = currentLocation,
        distanceInKm = 5.0
    )
)

HorusDataFacade.query(builder).fold(
    onSuccess = { nearbyPlaces ->
        // Process the nearby places
        nearbyPlaces.forEach { place ->
            println("Found: ${place["name"]}")
        }
    },
    onFailure = { error ->
        // Handle error
    }
)

Note: The coordinate extension uses a bounding box approximation for efficient filtering. This works well for distances up to several hundred kilometers.

Get a record by ID

To get a record by its ID, use the getById method passing the entity name and the record ID.

val userId = "0ca2caa1-74f1-4e58-a6a7-29e79efedfe4"

val user = HorusDataFacade.getById("users", userId)

if (user != null) {
//** YOUR CODE HERE WHEN RECORD EXISTS **/
}

Get count records in a entity

To get the number of records in an entity, use the countRecordFromEntity method passing the entity name.

HorusDataFacade.countRecordFromEntity("users").fold(
  onSuccess = { count ->
    //** YOUR CODE HERE WHEN SUCCESS */
  },
  onFailure = {
    //** YOUR CODE HERE WHEN FAILURE */
  }
)

Query Data shared

To query data shared by another user, use the queryShared method passing the entity name, the entity ID and a list of attributes to filter the data.

HorusDataFacade.queryDataShared(
  entityName,
  entityId,
  listOf(Horus.Attribute("attr", "value"))
)

Upload files

To upload files to the server is simple, use the uploadFile method passing the file data in bytes and then use the getFileUrl method to get the file URL to use where you need it.

val fileReference = HorusDataFacade.uploadFile(fileData)

val fileUrl = HorusDataFacade.getFileUrl(fileReference)

Utilities

Get entities name

To get the list of entities that are being managed by Horus, use the getEntityNames method.

val entityNames = HorusDataFacade.getEntityNames()

Force synchronization

To force the synchronization of the data with the server, use the forceSync method.

HorusDataFacade.forceSync(onSuccess = {
  /** YOUR CODE HERE WHEN SUCCESS */
}, onFailure = {
  /** YOUR CODE HERE WHEN FAILURE */
})

Validate if exists data to synchronize

To validate if there is data to synchronize, use the hasDataToSync method.

val hasDataToSync = HorusDataFacade.hasDataToSync()

Get the last synchronization date

To get the last synchronization date, use the getLastSyncDate method. This method will return a timestamp in seconds.

val lastSyncDate = HorusDataFacade.getLastSyncDate()

Authentication

Horus needs a user access token in session to be able to send the information to the remote server, for this use the HorusAuthentication class within the life cycle of the user session of your application to be able to configure the access token.

Setup access token

HorusAuthentication.setupUserAccessToken("{ACCESS_TOKEN}")  

Clear session

HorusAuthentication.clearSession()  

Setup to act as a guest user by another user

If the user must act on behalf of another user by invitation, the owner user of the entities must be configured as follows:

HorusAuthentication.setUserActingAs("{USER_OWNER_ID}")  

Entity restrictions

You can add specific restrictions for an entity, for example, to limit the number of records that can be stored in an entity. If the restriction is reached, the operation will fail and data result will be DataResult.NotAuthorized.

Use the setEntityRestrictions method of the HorusDataFacade class to set the restrictions.

 HorusDataFacade.setEntityRestrictions(
  listOf(
    MaxCountEntityRestriction("tasks", 100) // Limit to 100 records in the "tasks" entity
  )
)

Restrictions supported

  • MaxCountEntityRestriction: Limit the number of records that can be stored in an entity.

3. Local testing

Publish locally

../gradlew publishToMavenLocal

Location of the dependencies

C:\Users[User].m2\repository\com\apptank\horus\client\core