From 81bf267fc596bb19ae2663aeba8d0e2d1496544c Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Fri, 24 Jul 2026 01:22:14 +0530 Subject: [PATCH 01/12] SK-3006: Add FlowDB v2 collect and composable integration - Add FlowDBAPIClient, FlowDBCollectAPICallback, FlowDBCollectRequestBody, FlowDBMixedAPICallback for v2 insert/update/upsert API - Add FlowDBRevealApiCallback, FlowDBRevealRequestBody, FlowDBRevealRequestRecord for v2 detokenize API (reads "response" array, handles per-token httpCode) - Add UpdateType and UpsertOptions for upsert and update-by-skyflowID support - Update RevealElementInput: redaction changed from RedactionType enum to String to support custom vault redaction names - Update RevealValueCallback: fix key naming (records/errors), handle missing token field in error records gracefully - Update Client, CollectContainer, ComposableContainer, Configuration to wire FlowDB API client and support new collect/reveal options - Update collect/reveal containers and element inputs for FlowDB contract - Update tests to reflect FlowDB API changes; remove obsolete connection tests --- Skyflow/build.gradle | 1 + Skyflow/src/main/kotlin/Skyflow/Client.kt | 245 +-- .../main/kotlin/Skyflow/CollectContainer.kt | 159 +- .../kotlin/Skyflow/CollectElementInput.kt | 11 +- .../src/main/kotlin/Skyflow/CollectOptions.kt | 10 +- .../src/main/kotlin/Skyflow/Configuration.kt | 11 +- Skyflow/src/main/kotlin/Skyflow/Element.kt | 2 - .../main/kotlin/Skyflow/RevealContainer.kt | 10 +- .../main/kotlin/Skyflow/RevealElementInput.kt | 5 +- Skyflow/src/main/kotlin/Skyflow/UpdateType.kt | 6 + .../src/main/kotlin/Skyflow/UpsertOptions.kt | 7 + .../client/FlowDBCollectAPICallback.kt | 147 ++ .../client/FlowDBCollectRequestBody.kt | 109 + .../collect/client/FlowDBMixedAPICallback.kt | 77 + .../Skyflow/composable/ComposableContainer.kt | 97 +- .../kotlin/Skyflow/core/FlowDBAPIClient.kt | 72 + .../Skyflow/reveal/FlowDBRevealApiCallback.kt | 134 ++ .../Skyflow/reveal/FlowDBRevealRequestBody.kt | 40 + .../reveal/FlowDBRevealRequestRecord.kt | 9 + .../Skyflow/reveal/RevealValueCallback.kt | 22 +- .../com/Skyflow/CallbackResponseFormatTest.kt | 454 ---- .../com/Skyflow/CollectRequestBodyTest.kt | 479 ----- .../src/test/java/com/Skyflow/CollectTest.kt | 1809 ---------------- .../test/java/com/Skyflow/DetokenizeTests.kt | 293 --- Skyflow/src/test/java/com/Skyflow/GetTests.kt | 831 ------- .../java/com/Skyflow/InvokeConnectionTest.kt | 1511 ------------- .../src/test/java/com/Skyflow/ResponseTest.kt | 350 --- .../src/test/java/com/Skyflow/RevealTest.kt | 1007 --------- .../java/com/Skyflow/SoapConnectionTest.kt | 535 ----- .../test/java/com/Skyflow/TestApplication.kt | 8 + .../src/test/java/com/Skyflow/UnitTests.kt | 1901 ----------------- .../collect/client/UpdateBySkyflowIdTest.kt | 155 -- 32 files changed, 803 insertions(+), 9704 deletions(-) create mode 100644 Skyflow/src/main/kotlin/Skyflow/UpdateType.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt create mode 100644 Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestRecord.kt delete mode 100644 Skyflow/src/test/java/com/Skyflow/InvokeConnectionTest.kt delete mode 100644 Skyflow/src/test/java/com/Skyflow/SoapConnectionTest.kt delete mode 100644 Skyflow/src/test/java/com/Skyflow/UnitTests.kt diff --git a/Skyflow/build.gradle b/Skyflow/build.gradle index 1a5ecefb..cbff072e 100644 --- a/Skyflow/build.gradle +++ b/Skyflow/build.gradle @@ -17,6 +17,7 @@ ext { android { namespace "com.skyflow_android" compileSdk 35 + buildToolsVersion "35.0.0" defaultConfig { minSdk 21 diff --git a/Skyflow/src/main/kotlin/Skyflow/Client.kt b/Skyflow/src/main/kotlin/Skyflow/Client.kt index d465822d..2beffb11 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Client.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Client.kt @@ -2,234 +2,41 @@ package Skyflow import Skyflow.core.* import Skyflow.core.Logger -import Skyflow.get.GetOptions -import Skyflow.reveal.GetByIdRecord -import Skyflow.soap.SoapConnectionConfig import Skyflow.utils.Utils import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import org.json.JSONArray import org.json.JSONObject -import org.xml.sax.InputSource -import java.io.StringReader -import javax.xml.parsers.DocumentBuilderFactory -import kotlin.Exception import kotlin.reflect.KClass class Client internal constructor( val configuration: Configuration, -){ +) { internal val tag = Client::class.qualifiedName - internal val apiClient = APIClient(configuration.vaultID, configuration.vaultURL, - configuration.tokenProvider,configuration.options.logLevel) + internal val apiClient = FlowDBAPIClient( + configuration.vaultID, + configuration.vaultURL, + configuration.tokenProvider, + configuration.options.logLevel, + okHttpClient = configuration.okHttpClient + ) - internal val elementMap = HashMap() - fun insert(records: JSONObject, options: InsertOptions? = InsertOptions(), callback: Callback){ - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.INSERTING_RECORDS.getMessage(configuration.vaultID), configuration.options.logLevel) - apiClient.post(records, loggingCallback(callback, Messages.INSERTING_RECORDS_SUCCESS.getMessage(configuration.vaultID)), options!!) - } - catch (e:Exception) - { - callback.onFailure(e) - } - } - fun detokenize(records: JSONObject, callback: Callback) { - try { - Utils.checkVaultDetails(configuration) - this.apiClient.get(records,loggingCallback(callback, Messages.DETOKENIZE_SUCCESS.getMessage())) - } - catch (e:Exception) - { - callback.onFailure(Utils.constructError(e)) - } - } - - fun getById(records: JSONObject, callback: Callback) - { - Logger.info(tag, Messages.GET_BY_ID_CALLED.getMessage(), configuration.options.logLevel) - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.GET_BY_ID_CALLED.getMessage(), configuration.options.logLevel) - val result = constructBodyForGetById(records) - this.apiClient.getById(result, callback) - } catch (e: Exception) { - callback.onFailure(Utils.constructError(e)) - } - } - - fun get(records: JSONObject, options: GetOptions?, callback: Callback) { - Logger.info(tag, Messages.GET_CALLED.getMessage(), configuration.options.logLevel) - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.GETTING_RECORDS.getMessage(), configuration.options.logLevel) - this.apiClient.get(records, options, callback) - } catch (e: Exception) { - callback.onFailure(Utils.constructError(e)) - } - } + internal val elementMap = HashMap() - @Deprecated("Support for this method will be removed soon. Please use any of the Server Side SDKs to invoke a connection", level = DeprecationLevel.WARNING) - internal fun invokeConnection(connectionConfig: ConnectionConfig, callback: Callback) { - try { - Logger.info(tag, Messages.INVOKE_CONNECTION_CALLED.getMessage(), configuration.options.logLevel) - val checkUrl = Utils.checkUrl(connectionConfig.connectionURL) - if(connectionConfig.connectionURL.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_CONNECTION_URL,tag, configuration.options.logLevel) - if (!checkUrl) - throw SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL,tag, configuration.options.logLevel, arrayOf(connectionConfig.connectionURL)) - this.apiClient.invokeConnection(connectionConfig, callback,this) - } - catch (e:Exception) - { - callback.onFailure(Utils.constructError(e)) - } + // Internal stub kept for dormant PDB code (ConnectionApiCallback, SoapApiCallback) to compile. + // Not part of the public API and not called by any live code path. + @Suppress("unused") + internal fun detokenize(records: JSONObject, callback: Callback) { + callback.onFailure(Utils.constructError(Exception("detokenize is not available in FlowDB mode"))) } - @Deprecated("Support for this method will be removed soon. Please contact admin", level = DeprecationLevel.WARNING) - internal fun invokeSoapConnection(soapConnectionConfig: SoapConnectionConfig,callback: Callback) { - try { - validateSoapConnectionDetails(soapConnectionConfig) - this.apiClient.invokeSoapConnection(soapConnectionConfig, this, callback) - } - catch (e: Exception){ - callback.onFailure(e) - } - } - internal fun validateSoapConnectionDetails(soapConnectionConfig: SoapConnectionConfig) - { - if (soapConnectionConfig.connectionURL.isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_CONNECTION_URL, Utils.tag, configuration.options.logLevel) - } - if (!Utils.checkUrl(soapConnectionConfig.connectionURL)) { - throw SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL, - Utils.tag, configuration.options.logLevel, params = arrayOf(soapConnectionConfig.connectionURL)) - } - if (soapConnectionConfig.requestXML.trim().isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_REQUEST_XML, Utils.tag, configuration.options.logLevel) - } - if (soapConnectionConfig.requestXML.trim().isNotEmpty()) { - try { - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(soapConnectionConfig.requestXML))); - } catch (e: Exception) { - throw SkyflowError(SkyflowErrorCode.INVALID_REQUEST_XML, Utils.tag, configuration.options.logLevel, params = arrayOf(e.message.toString())) - } - } - if (soapConnectionConfig.responseXML.trim().isNotEmpty()) { - try { - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(soapConnectionConfig.responseXML))); - } catch (e: Exception) { - throw SkyflowError(SkyflowErrorCode.INVALID_RESPONSE_XML, Utils.tag, configuration.options.logLevel, params = arrayOf(e.message.toString())) - } - } - } - - internal fun constructBodyForGetById(records: JSONObject): MutableList { - if (!records.has("records")) - throw SkyflowError( - SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, - tag, configuration.options.logLevel - ) - else if (records.get("records") !is JSONArray) - throw SkyflowError(SkyflowErrorCode.INVALID_RECORDS, tag, configuration.options.logLevel) - val jsonArray = records.getJSONArray("records") - if (jsonArray.length() == 0) - throw SkyflowError(SkyflowErrorCode.EMPTY_RECORDS, tag, configuration.options.logLevel) - var i = 0 - val result = mutableListOf() - while (i < jsonArray.length()) { - val jsonObj = jsonArray.getJSONObject(i) - if (jsonObj == {}) - throw SkyflowError( - SkyflowErrorCode.EMPTY_RECORD_OBJECT, tag, configuration.options.logLevel, - arrayOf("$i") - ) - else if (!jsonObj.has("table")) { - throw SkyflowError( - SkyflowErrorCode.TABLE_KEY_NOY_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.get("table") !is String) { - throw SkyflowError( - SkyflowErrorCode.INVALID_TABLE_NAME, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!jsonObj.has("redaction")) { - throw SkyflowError( - SkyflowErrorCode.REDACTION_KEY_NOT_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!jsonObj.has("ids")) { - throw SkyflowError( - SkyflowErrorCode.IDS_KEY_NOT_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.getString("table").isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_TABLE_KEY, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.getString("redaction").isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_REDACTION_VALUE, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!(jsonObj.get("redaction").toString() == "PLAIN_TEXT" || - jsonObj.get("redaction").toString() == "DEFAULT" || - jsonObj.get("redaction").toString() == "MASKED" || - jsonObj.get("redaction").toString() == "REDACTED") - ) { - throw SkyflowError( - SkyflowErrorCode.INVALID_REDACTION_TYPE, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else { - var skyflowIds = jsonObj.get("ids") - try { - skyflowIds = skyflowIds as ArrayList - } catch (e: Exception) { - throw SkyflowError( - SkyflowErrorCode.INVALID_IDS, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - if (skyflowIds.isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_RECORD_IDS, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - for (j in 0 until skyflowIds.size) { - if (skyflowIds[j].isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_ID_IN_RECORD_IDS, - tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - } - val record = GetByIdRecord( - skyflowIds, - jsonObj.get("table").toString(), - jsonObj.get("redaction").toString() - ) - result.add(record) - } - i++ - } - return result - } - - fun container(type: KClass) : Container{ - if(type == ContainerType.COLLECT){ + fun container(type: KClass): Container { + if (type == ContainerType.COLLECT) { Logger.info(tag, Messages.COLLECT_CONTAINER_CREATED.getMessage(), configuration.options.logLevel) - } - else if(type == ContainerType.REVEAL){ + } else if (type == ContainerType.REVEAL) { Logger.info(tag, Messages.REVEAL_CONTAINER_CREATED.getMessage(), configuration.options.logLevel) } - return Container(configuration,this) + return Container(configuration, this) } fun container( @@ -249,20 +56,4 @@ class Client internal constructor( } return Container(configuration, this, context, options) } - - inner class loggingCallback( - private val clientCallback: Callback, - private val successMessage: String, - ) : Callback { - override fun onSuccess(responseBody: Any) { - Logger.info(tag, successMessage, configuration.options.logLevel) - clientCallback.onSuccess(responseBody) - } - override fun onFailure(exception: Any) { - clientCallback.onFailure(exception) - } - } } - - - diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt index a93115fb..3a8dd119 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt @@ -1,13 +1,14 @@ package Skyflow -import Skyflow.collect.client.CollectRequestBody +import Skyflow.collect.client.FlowDBCollectRequestBody +import Skyflow.collect.client.FlowDBMixedAPICallback +import org.json.JSONObject import Skyflow.core.Logger import Skyflow.core.Messages import Skyflow.core.getMessage import Skyflow.utils.Utils import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import org.json.JSONObject import java.util.* open class CollectContainer : ContainerProtocol { @@ -21,16 +22,8 @@ fun Container.create( options: CollectElementOptions = CollectElementOptions() ): TextField { Utils.checkInputFormatOptions(input.type, options, configuration.options.logLevel) - Logger.info( - tag, - Messages.VALIDATE_INPUT_FORMAT_OPTIONS.getMessage(input.label), - configuration.options.logLevel - ) - Logger.info( - tag, - Messages.CREATED_COLLECT_ELEMENT.getMessage(input.label), - configuration.options.logLevel - ) + Logger.info(tag, Messages.VALIDATE_INPUT_FORMAT_OPTIONS.getMessage(input.label), configuration.options.logLevel) + Logger.info(tag, Messages.CREATED_COLLECT_ELEMENT.getMessage(input.label), configuration.options.logLevel) val collectElement = TextField(context, configuration.options, collectElements.size) collectElement.setupField(input, options) collectElements.add(collectElement) @@ -40,61 +33,56 @@ fun Container.create( return collectElement } -fun Container.collect(callback: Callback, options: CollectOptions? = CollectOptions()){ +fun Container.collect(callback: Callback, options: CollectOptions? = CollectOptions()) { try { - Utils.checkVaultDetails(client.configuration) + validateVaultConfig() Logger.info(tag, Messages.VALIDATE_COLLECT_RECORDS.getMessage(), configuration.options.logLevel) validateElements() - post(callback,options) - } - catch (e:Exception) - { + post(callback, options) + } catch (e: Exception) { callback.onFailure(Utils.constructErrorResponse(e)) } } + +internal fun Container.validateVaultConfig() { + if (configuration.vaultID.isEmpty()) { + throw SkyflowError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowError(SkyflowErrorCode.INVALID_VAULT_URL, tag, configuration.options.logLevel) + } +} + internal fun Container.validateElements() { var errors = "" for (element in this.collectElements) { - errors = validateElement(element,errors) + errors = validateElement(element, errors) } if (errors != "") { throw SkyflowError(SkyflowErrorCode.INVALID_INPUT, tag, configuration.options.logLevel, arrayOf(errors)) } } -internal fun Container.validateElement(element: TextField,err:String) : String -{ +internal fun Container.validateElement(element: TextField, err: String): String { var errorOnElement = err if (!element.isAttachedToWindow()) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, - tag, - configuration.options.logLevel, - arrayOf(element.columnName)) + throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, configuration.options.logLevel, arrayOf(element.columnName)) } when { element.collectInput.table.equals(null) -> { - throw SkyflowError(SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowError(SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.column.equals(null) -> { - throw SkyflowError(SkyflowErrorCode.MISSING_COLUMN, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowError(SkyflowErrorCode.MISSING_COLUMN, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.table!!.isEmpty() -> { - throw SkyflowError(SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowError(SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.column!!.isEmpty() -> { - throw SkyflowError(SkyflowErrorCode.EMPTY_COLUMN_NAME, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowError(SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } else -> { val state = element.getState() @@ -107,45 +95,64 @@ internal fun Container.validateElement(element: TextField,err: } return errorOnElement } -internal fun Container.post(callback:Callback,options: CollectOptions?) -{ - // Separate insert and update elements/records - val (insertElements, insertAdditionalFields, updateRecords) = Skyflow.collect.client.CollectRequestBody.separateInsertAndUpdateRecords( + +internal fun Container.post(callback: Callback, options: CollectOptions?) { + val collectOptions = options ?: CollectOptions() + val skyflowIds = collectOptions.skyflowIds + + if (!skyflowIds.isNullOrEmpty()) { + val updateElements = collectElements.filter { skyflowIds.containsKey(it.tableName) } + val insertElements = collectElements.filter { !skyflowIds.containsKey(it.tableName) } + + // One update request body per table (API has tableName at root) + val updateBodies = updateElements.groupBy { it.tableName }.map { (tableName, elements) -> + FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowIds[tableName]!!, configuration.options.logLevel + ) + } + + // One insert/upsert body for the remaining elements (if any) + val insertBody: JSONObject? = if (insertElements.isNotEmpty()) { + val insertOptions = CollectOptions( + tokens = collectOptions.tokens, + upsert = collectOptions.upsert?.filter { !skyflowIds.containsKey(it.tableName) } + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, updateBodies, insertBody, callback, collectOptions, + configuration.options.logLevel + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + collectOptions, configuration.options.logLevel ) - - val hasInsertData = insertElements.isNotEmpty() || insertAdditionalFields != null - val hasUpdateRecords = updateRecords.isNotEmpty() - - if (hasInsertData && hasUpdateRecords) { - // Mixed case: both insert and update - val insertRecordsJson = if (insertElements.isNotEmpty()) { - JSONObject(Skyflow.collect.client.CollectRequestBody.createRequestBody( - insertElements, - insertAdditionalFields, - configuration.options.logLevel - )) - } else { - insertAdditionalFields - } - - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(insertRecordsJson, updateRecords, callback, insertOptions) - } else if (hasUpdateRecords) { - // Only update records - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(null, updateRecords, callback, insertOptions) - } else { - // Only insert records - val records = Skyflow.collect.client.CollectRequestBody.createRequestBody( - this.collectElements, - insertAdditionalFields, + this.client.apiClient.post(requestBody, callback, collectOptions) +} + +fun Container.update(tableName: String, skyflowID: String, callback: Callback, options: CollectOptions = CollectOptions()) { + try { + validateVaultConfig() + val requestBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, + tableName, + this.collectElements, + skyflowID, configuration.options.logLevel ) - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.post(JSONObject(records), callback, insertOptions) + this.client.apiClient.post(requestBody, callback, options, "update") + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e)) } } - diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt index e2eef7de..0b877c2b 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt @@ -10,8 +10,7 @@ class CollectElementInput( internal var errorTextStyles: Styles = Styles(), internal var label: String = "", internal var placeholder: String = "", - internal var validations: ValidationSet = ValidationSet(), - internal var skyflowID: String? = null + internal var validations: ValidationSet = ValidationSet() ) { internal lateinit var type: SkyflowElementType @@ -32,8 +31,7 @@ class CollectElementInput( label: String = "", placeholder: String = "", altText: String = "", - validations: ValidationSet = ValidationSet(), - skyflowID: String? = null + validations: ValidationSet = ValidationSet() ) : this( table, column, @@ -42,10 +40,9 @@ class CollectElementInput( errorTextStyles, label, placeholder, - validations, - skyflowID + validations ) { this.type = type this.altText = altText } -} \ No newline at end of file +} diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt b/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt index a07417cf..476b1974 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt @@ -1,7 +1,7 @@ package Skyflow -import org.json.JSONArray -import org.json.JSONObject - -class CollectOptions(val token:Boolean = true, val additionalFields: JSONObject? = null, val upsert : JSONArray? = null) { -} \ No newline at end of file +class CollectOptions( + val tokens: Boolean = true, + val upsert: List? = null, + val skyflowIds: Map? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/Configuration.kt b/Skyflow/src/main/kotlin/Skyflow/Configuration.kt index 9fb525ac..36581ab8 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Configuration.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Configuration.kt @@ -7,12 +7,5 @@ class Configuration( var vaultURL: String = "", val tokenProvider: TokenProvider, val options: Options = Options(), -){ - init { - if( vaultURL.endsWith("/")){ - vaultURL += "v1/vaults/" - } else{ - vaultURL += "/v1/vaults/" - } - } -} \ No newline at end of file + val okHttpClient: okhttp3.OkHttpClient = okhttp3.OkHttpClient(), +) \ No newline at end of file diff --git a/Skyflow/src/main/kotlin/Skyflow/Element.kt b/Skyflow/src/main/kotlin/Skyflow/Element.kt index e120f272..6c2c08cc 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Element.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Element.kt @@ -35,8 +35,6 @@ open class Element @JvmOverloads constructor( tableName = this.collectInput.table!! if(!this.collectInput.column.equals(null)) columnName = this.collectInput.column!! - if(!this.collectInput.skyflowID.equals(null)) - skyflowID = this.collectInput.skyflowID isRequired = this.options.required state = State(columnName,isRequired) } diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt b/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt index 5efb4e89..d793ee68 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt @@ -5,7 +5,7 @@ import Skyflow.core.Messages import Skyflow.core.getMessage import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import Skyflow.reveal.RevealRequestBody +import Skyflow.reveal.FlowDBRevealRequestBody import Skyflow.reveal.RevealValueCallback import Skyflow.utils.Utils import Skyflow.utils.Utils.Companion.checkIfElementsMounted @@ -45,7 +45,6 @@ fun Container.reveal( options: RevealOptions? = RevealOptions() ) { try { - Utils.checkVaultDetails(client.configuration) validateElements() Logger.info( tag, @@ -91,6 +90,9 @@ internal fun Container.get(callback: Callback, options: RevealO this.revealElements, configuration.options.logLevel ) - val records = RevealRequestBody.createRequestBody(this.revealElements) - this.client.apiClient.get(records, revealValueCallback) + val requestBody = FlowDBRevealRequestBody.buildRequestBody( + configuration.vaultID, + this.revealElements + ) + this.client.apiClient.get(requestBody, revealValueCallback) } \ No newline at end of file diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt b/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt index 1e132a16..8f542f53 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt @@ -2,10 +2,11 @@ package Skyflow class RevealElementInput( internal var token: String? = null, - internal var redaction: RedactionType? = RedactionType.PLAIN_TEXT, + internal var tokenGroupName: String? = null, + internal var redaction: String? = null, internal var inputStyles: Styles = Styles(), internal var labelStyles: Styles = Styles(), internal var errorTextStyles: Styles = Styles(), internal var label: String = "", internal var altText: String = "" -) {} \ No newline at end of file +) {} diff --git a/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt b/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt new file mode 100644 index 00000000..57989bdf --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt @@ -0,0 +1,6 @@ +package Skyflow + +enum class UpdateType { + UPDATE, + REPLACE +} diff --git a/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt b/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt new file mode 100644 index 00000000..293eb9d7 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt @@ -0,0 +1,7 @@ +package Skyflow + +data class UpsertOptions( + val tableName: String, + val updateType: UpdateType, + val uniqueColumns: List +) diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt new file mode 100644 index 00000000..49bed1b2 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt @@ -0,0 +1,147 @@ +package Skyflow.collect.client + +import Skyflow.* +import Skyflow.core.FlowDBAPIClient +import Skyflow.core.Logger +import Skyflow.core.Messages +import Skyflow.core.getMessage +import Skyflow.utils.Utils +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException + +internal class FlowDBCollectAPICallback( + private val apiClient: FlowDBAPIClient, + private val requestBody: JSONObject, + val callback: Skyflow.Callback, + private val options: CollectOptions, + val logLevel: LogLevel, + private val endpoint: String = "insert" +) : Skyflow.Callback { + private val okHttpClient = apiClient.okHttpClient + private val tag = FlowDBCollectAPICallback::class.qualifiedName + + override fun onSuccess(responseBody: Any) { + try { + Logger.info(tag, Messages.VALIDATE_RECORDS.getMessage(), logLevel) + val body = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) + val metrics = Utils.fetchMetrics() + val url = "${apiClient.vaultURL.trimEnd('/')}/v2/records/$endpoint" + val request = Request.Builder() + .method("POST", body) + .addHeader("Authorization", "$responseBody") + .addHeader("sky-metadata", "$metrics") + .url(url) + .build() + sendRequest(request) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e)) + } + } + + override fun onFailure(exception: Any) { + callback.onFailure(exception) + } + + private fun sendRequest(request: Request) { + okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: IOException) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + + override fun onResponse(call: okhttp3.Call, response: Response) { + verifyResponse(response) + } + }) + } + + private fun verifyResponse(response: Response) { + response.use { + try { + val bodyStr = response.body?.string() ?: "" + if (!response.isSuccessful) { + val parsed = try { JSONObject(bodyStr) } catch (e: Exception) { null } + if (parsed != null && parsed.has("records")) { + dispatchResponse(bodyStr) + return + } + val message = try { + parsed?.getJSONObject("error")?.getString("message") ?: bodyStr + } catch (e: JSONException) { bodyStr } + val requestId = response.headers["x-request-id"] ?: "" + callback.onFailure(Utils.constructErrorResponse(response.code, Utils.appendRequestId(message, requestId))) + return + } + dispatchResponse(bodyStr) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + } + } + + private fun dispatchResponse(bodyStr: String) { + val responseJson = JSONObject(bodyStr) + val records = responseJson.optJSONArray("records") ?: JSONArray() + val successRecords = JSONArray() + val errorRecords = JSONArray() + + for (i in 0 until records.length()) { + val record = records.getJSONObject(i) + val httpCode = record.optInt("httpCode", 200) + if (httpCode != 200) { + errorRecords.put( + JSONObject() + .put("error", record.optString("error", "")) + .put("skyflowID", record.opt("skyflowID")) + .put("tableName", record.optString("tableName", "")) + .put("httpCode", httpCode) + ) + continue + } + val skyflowID = record.optString("skyflowID", "") + val tableName = record.optString("tableName", "") + val fieldsObject = JSONObject().put("skyflow_id", skyflowID) + + if (options.tokens) { + val tokensObj = record.optJSONObject("tokens") + if (tokensObj != null) { + val fieldNames = tokensObj.keys() + while (fieldNames.hasNext()) { + val fieldName = fieldNames.next() + fieldsObject.put(fieldName, tokensObj.getJSONArray(fieldName)) + } + } + } + + val resultRecord = JSONObject() + .put("table", tableName) + .put("fields", fieldsObject) + .put("httpCode", httpCode) + val hashedData = record.optJSONObject("hashedData") + if (hashedData != null) resultRecord.put("hashedData", hashedData) + successRecords.put(resultRecord) + } + + val result = JSONObject() + when { + errorRecords.length() == 0 -> { + result.put("records", successRecords) + callback.onSuccess(result) + } + successRecords.length() == 0 -> { + result.put("errors", errorRecords) + callback.onFailure(result) + } + else -> { + result.put("records", successRecords) + result.put("errors", errorRecords) + callback.onFailure(result) + } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt new file mode 100644 index 00000000..e8494332 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt @@ -0,0 +1,109 @@ +package Skyflow.collect.client + +import Skyflow.* +import Skyflow.collect.elements.validations.ElementValueMatchRule +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBCollectRequestBody { + companion object { + private val tag = FlowDBCollectRequestBody::class.qualifiedName + + internal fun buildRequestBody( + vaultID: String, + elements: MutableList, + options: CollectOptions, + logLevel: LogLevel + ): JSONObject { + val tableMap = groupByTable(elements, logLevel) + val upsertByTable = options.upsert?.associateBy { it.tableName } ?: emptyMap() + + val recordsArray = JSONArray() + for ((tableName, columns) in tableMap) { + val dataObject = JSONObject() + for (record in columns) { + createJSONKey(dataObject, record.columnName, record.value) + } + val recordObj = JSONObject().put("data", dataObject).put("tableName", tableName) + val upsertOpt = upsertByTable[tableName] + if (upsertOpt != null) { + recordObj.put("upsert", JSONObject() + .put("updateType", upsertOpt.updateType.name) + .put("uniqueColumns", JSONArray(upsertOpt.uniqueColumns))) + } + recordsArray.put(recordObj) + } + return JSONObject().put("vaultID", vaultID).put("records", recordsArray) + } + + internal fun buildUpdateRequestBody( + vaultID: String, + tableName: String, + elements: MutableList, + skyflowID: String, + logLevel: LogLevel + ): JSONObject { + val tableMap = groupByTable(elements, logLevel) + val tableElements = tableMap[tableName] ?: mutableListOf() + val dataObject = JSONObject() + for (record in tableElements) { + createJSONKey(dataObject, record.columnName, record.value) + } + return JSONObject() + .put("vaultID", vaultID) + .put("tableName", tableName) + .put("records", JSONArray().put(JSONObject().put("skyflowID", skyflowID).put("data", dataObject))) + } + + private fun groupByTable( + elements: MutableList, + logLevel: LogLevel + ): LinkedHashMap> { + val tableMap = LinkedHashMap>() + val tableWithColumn = HashSet() + for (element in elements) { + val tableName = element.tableName + if (tableMap[tableName] != null) { + if (tableWithColumn.contains(tableName + element.columnName)) { + var hasElementValueMatchRule = false + for (validation in element.collectInput.validations.rules) { + if (validation is ElementValueMatchRule) { + hasElementValueMatchRule = true + break + } + } + if (!hasElementValueMatchRule) + throw SkyflowError( + SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, + arrayOf(tableName, element.columnName) + ) + continue + } + tableWithColumn.add(tableName + element.columnName) + tableMap[tableName]!!.add(CollectRequestRecord(element.columnName, element.getValue())) + } else { + tableWithColumn.add(tableName + element.columnName) + tableMap[tableName] = mutableListOf(CollectRequestRecord(element.columnName, element.getValue())) + } + } + return tableMap + } + + private fun createJSONKey(obj: JSONObject, columnName: String, value: Any) { + val keys = columnName.split(".").toTypedArray() + if (obj.has(keys[0])) { + if (keys.size > 1) { + createJSONKey(obj.get(keys[0]) as JSONObject, keys.drop(1).joinToString("."), value) + } + } else { + if (keys.size > 1) { + val tempObject = JSONObject() + obj.put(keys[0], tempObject) + createJSONKey(tempObject, keys.drop(1).joinToString("."), value) + } else { + obj.put(keys[0], value) + } + } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt new file mode 100644 index 00000000..54bd59cb --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt @@ -0,0 +1,77 @@ +package Skyflow.collect.client + +import Skyflow.Callback +import Skyflow.CollectOptions +import Skyflow.LogLevel +import Skyflow.core.FlowDBAPIClient +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBMixedAPICallback( + private val apiClient: FlowDBAPIClient, + private val updateBodies: List, + private val insertBody: JSONObject?, + private val finalCallback: Callback, + private val options: CollectOptions, + val logLevel: LogLevel +) : Callback { + + private val totalCalls = updateBodies.size + if (insertBody != null) 1 else 0 + private var completedCalls = 0 + private val allRecords = JSONArray() + private val allErrors = JSONArray() + + override fun onSuccess(responseBody: Any) { + val token = responseBody.toString() + for (body in updateBodies) { + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "update") + .onSuccess(token) + } + insertBody?.let { body -> + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "insert") + .onSuccess(token) + } + } + + override fun onFailure(exception: Any) { + finalCallback.onFailure(exception) + } + + private fun makeSubCallback(): Callback = object : Callback { + override fun onSuccess(responseBody: Any) { + synchronized(this@FlowDBMixedAPICallback) { + val json = JSONObject(responseBody.toString()) + mergeInto(allRecords, json.optJSONArray("records")) + completedCalls++ + if (completedCalls == totalCalls) dispatch() + } + } + + override fun onFailure(exception: Any) { + synchronized(this@FlowDBMixedAPICallback) { + try { + val json = JSONObject(exception.toString()) + mergeInto(allRecords, json.optJSONArray("records")) + mergeInto(allErrors, json.optJSONArray("errors")) + } catch (e: Exception) { + allErrors.put(JSONObject().put("error", exception.toString())) + } + completedCalls++ + if (completedCalls == totalCalls) dispatch() + } + } + } + + private fun mergeInto(target: JSONArray, source: JSONArray?) { + source ?: return + for (i in 0 until source.length()) target.put(source[i]) + } + + private fun dispatch() { + val result = JSONObject() + if (allRecords.length() > 0) result.put("records", allRecords) + if (allErrors.length() > 0) result.put("errors", allErrors) + if (allErrors.length() > 0) finalCallback.onFailure(result) + else finalCallback.onSuccess(result) + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt index 35d2b102..006e6626 100644 --- a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt @@ -1,7 +1,9 @@ package Skyflow.composable import Skyflow.* -import Skyflow.collect.client.CollectRequestBody +import Skyflow.collect.client.FlowDBCollectRequestBody +import Skyflow.collect.client.FlowDBMixedAPICallback +import org.json.JSONArray import Skyflow.core.Logger import Skyflow.core.Messages import Skyflow.core.getMessage @@ -90,7 +92,7 @@ fun Container.collect( options: CollectOptions? = CollectOptions() ) { try { - Utils.checkVaultDetails(client.configuration) + validateVaultConfig() Logger.info( tag, Messages.VALIDATE_COLLECT_RECORDS.getMessage(), @@ -103,6 +105,18 @@ fun Container.collect( } } +private fun Container.validateVaultConfig() { + if (configuration.vaultID.isEmpty()) { + throw SkyflowError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowError(SkyflowErrorCode.INVALID_VAULT_URL, tag, configuration.options.logLevel) + } +} + private fun Container.validateElements() { var errors = "" for (element in this.collectElements) { @@ -175,43 +189,58 @@ private fun Container.validateElement( } private fun Container.post(callback: Callback, options: CollectOptions?) { - // Separate insert and update elements/records (including additionalFields) - val (insertElements, insertAdditionalFields, updateRecords) = CollectRequestBody.separateInsertAndUpdateRecords( + val collectOptions = options ?: CollectOptions() + val skyflowIds = collectOptions.skyflowIds + + if (!skyflowIds.isNullOrEmpty()) { + val updateElements = collectElements.filter { skyflowIds.containsKey(it.tableName) } + val insertElements = collectElements.filter { !skyflowIds.containsKey(it.tableName) } + + val updateBodies = updateElements.groupBy { it.tableName }.map { (tableName, elements) -> + FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowIds[tableName]!!, configuration.options.logLevel + ) + } + + val insertBody: JSONObject? = if (insertElements.isNotEmpty()) { + val insertOptions = CollectOptions( + tokens = collectOptions.tokens, + upsert = collectOptions.upsert?.filter { !skyflowIds.containsKey(it.tableName) } + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, updateBodies, insertBody, callback, collectOptions, + configuration.options.logLevel + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + collectOptions, configuration.options.logLevel ) - - val hasInsertData = insertElements.isNotEmpty() || insertAdditionalFields != null - val hasUpdateRecords = updateRecords.isNotEmpty() - - if (hasInsertData && hasUpdateRecords) { - // Mixed case: both insert and update - val insertRecordsJson = if (insertElements.isNotEmpty()) { - JSONObject(CollectRequestBody.createRequestBody( - insertElements, - insertAdditionalFields, - configuration.options.logLevel - )) - } else { - insertAdditionalFields - } - - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(insertRecordsJson, updateRecords, callback, insertOptions) - } else if (hasUpdateRecords) { - // Only update records - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(null, updateRecords, callback, insertOptions) - } else { - // Only insert records - val records = CollectRequestBody.createRequestBody( - this.collectElements, - insertAdditionalFields, + this.client.apiClient.post(requestBody, callback, collectOptions) +} + +fun Container.update(tableName: String, skyflowID: String, callback: Callback, options: CollectOptions = CollectOptions()) { + try { + validateVaultConfig() + val requestBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, this.collectElements, skyflowID, configuration.options.logLevel ) - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.post(JSONObject(records), callback, insertOptions) + this.client.apiClient.post(requestBody, callback, options, "update") + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e)) } } diff --git a/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt new file mode 100644 index 00000000..7faebf1a --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt @@ -0,0 +1,72 @@ +package Skyflow.core + +import Skyflow.* +import Skyflow.collect.client.FlowDBCollectAPICallback +import Skyflow.reveal.FlowDBRevealApiCallback +import Skyflow.utils.Utils +import org.json.JSONObject + +internal class FlowDBAPIClient( + val vaultId: String, + val vaultURL: String, + private val tokenProvider: TokenProvider, + val logLevel: LogLevel, + private var token: String = "", + val okHttpClient: okhttp3.OkHttpClient = okhttp3.OkHttpClient() +) { + private val tag = FlowDBAPIClient::class.qualifiedName + + private fun isValidToken(token: String?): Boolean { + return if (token != "") !JWTUtils.isExpired(token!!) else false + } + + fun getAccessToken(callback: Callback) { + try { + if (!isValidToken(token)) { + Logger.info(tag, Messages.RETRIEVING_BEARER_TOKEN.getMessage(), logLevel) + tokenProvider.getBearerToken(object : Callback { + override fun onSuccess(responseBody: Any) { + Logger.info(tag, Messages.BEARER_TOKEN_RECEIVED.getMessage(), logLevel) + if (!isValidToken(responseBody.toString())) { + callback.onFailure(SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel)) + } else { + token = "Bearer $responseBody" + callback.onSuccess(token) + } + } + + override fun onFailure(exception: Any) { + Logger.error(tag, Messages.RETRIEVING_BEARER_TOKEN_FAILED.getMessage(), logLevel) + callback.onFailure(SkyflowError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel)) + } + }) + } else { + callback.onSuccess(token) + } + } catch (e: Exception) { + callback.onFailure(SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel)) + } + } + + fun post(requestBody: JSONObject, callback: Callback, options: CollectOptions, endpoint: String = "insert") { + try { + val collectApiCallback = FlowDBCollectAPICallback(this, requestBody, callback, options, logLevel, endpoint) + this.getAccessToken(collectApiCallback) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } + + fun get(requestBody: JSONObject, callback: Callback) { + try { + val tokensArray = requestBody.optJSONArray("tokens") + if (tokensArray == null || tokensArray.length() == 0) { + throw SkyflowError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) + } + val revealApiCallback = FlowDBRevealApiCallback(callback, this, requestBody) + this.getAccessToken(revealApiCallback) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt new file mode 100644 index 00000000..6f1098d8 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt @@ -0,0 +1,134 @@ +package Skyflow.reveal + +import Skyflow.Callback +import Skyflow.core.FlowDBAPIClient +import Skyflow.utils.Utils +import okhttp3.Call +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException + +internal class FlowDBRevealApiCallback( + private val callback: Callback, + private val apiClient: FlowDBAPIClient, + private val requestBody: JSONObject +) : Callback { + private val tag = FlowDBRevealApiCallback::class.qualifiedName + private val okHttpClient = apiClient.okHttpClient + + override fun onSuccess(responseBody: Any) { + try { + val url = "${apiClient.vaultURL.trimEnd('/')}/v2/tokens/detokenize" + val body = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) + val metrics = Utils.fetchMetrics() + val request = Request.Builder() + .method("POST", body) + .addHeader("Authorization", "$responseBody") + .addHeader("sky-metadata", "$metrics") + .url(url) + .build() + sendRequest(request) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } + + override fun onFailure(exception: Any) { + callback.onFailure(exception) + } + + private fun sendRequest(request: Request) { + okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { + override fun onFailure(call: Call, e: IOException) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + + override fun onResponse(call: Call, response: Response) { + verifyResponse(response) + } + }) + } + + private fun verifyResponse(response: Response) { + response.use { + try { + val bodyStr = response.body?.string() ?: "" + val responseJson = try { JSONObject(bodyStr) } catch (e: JSONException) { null } + + if (responseJson?.has("response") == true) { + parseAndDispatch(responseJson) + return + } + + // Whole-request failure (auth error, malformed request, etc.) + if (!response.isSuccessful) { + val message = try { + responseJson?.getJSONObject("error")?.getString("message") ?: bodyStr + } catch (e: JSONException) { bodyStr } + val requestId = response.headers["x-request-id"] ?: "" + callback.onFailure(Utils.constructErrorResponse( + response.code, + Utils.appendRequestId(message, requestId) + )) + return + } + + parseAndDispatch(responseJson ?: JSONObject()) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + } + } + + private fun parseAndDispatch(responseJson: JSONObject) { + val responseArray = responseJson.optJSONArray("response") ?: JSONArray() + val successRecords = JSONArray() + val errorRecords = JSONArray() + + for (i in 0 until responseArray.length()) { + val entry = responseArray.getJSONObject(i) + val httpCode = entry.optInt("httpCode", 200) + val originalToken = entry.optString("token") + + if (httpCode == 200) { + successRecords.put( + JSONObject() + .put("token", originalToken) + .put("value", entry.optString("value")) + .put("tokenGroupName", entry.optString("tokenGroupName")) + .put("httpCode", httpCode) + .put("metadata", entry.optJSONObject("metadata")) + ) + } else { + errorRecords.put( + JSONObject() + .put("token", originalToken) + .put("error", entry.optString("error", "Detokenize failed")) + .put("httpCode", httpCode) + ) + } + } + + val result = JSONObject() + when { + errorRecords.length() == 0 -> { + result.put("records", successRecords) + callback.onSuccess(result) + } + successRecords.length() == 0 -> { + result.put("errors", errorRecords) + callback.onFailure(result) + } + else -> { + result.put("records", successRecords) + result.put("errors", errorRecords) + callback.onFailure(result) + } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt new file mode 100644 index 00000000..005bd46e --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt @@ -0,0 +1,40 @@ +package Skyflow.reveal + +import Skyflow.Label +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBRevealRequestBody { + companion object { + internal fun buildRequestBody(vaultID: String, elements: MutableList