diff --git a/.gitignore b/.gitignore index 8ccc4f8..12ec793 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ .externalNativeBuild .cxx local.properties + +flowvault-samples/ diff --git a/README.md b/README.md index 8282a99..448eae1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Skyflow’s android SDK can be used to securely collect, tokenize, and display s * [Securely collecting data client-side](#securely-collecting-data-client-side) * [Securely collecting data client-side using composable elements](#securely-collecting-data-client-side-using-composable-elements) * [Securely revealing data client-side](#securely-revealing-data-client-side) +* [Typed callbacks and response handling](#typed-callbacks-and-response-handling) + * [Collect with typed callbacks](#collect-with-typed-callbacks) + * [Reveal with typed callbacks](#reveal-with-typed-callbacks) # Installation @@ -200,7 +203,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env --- # Securely collecting data client-side -- [**Inserting data into the vault**](#inserting-data-into-the-vault) - [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) - [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) - [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) @@ -208,64 +210,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env - [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) -## Inserting data into the vault - -To insert data into the vault from the integrated application, use the ```insert(records: JSONObject, options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback)``` method of the Skyflow client. The records parameter takes a JSON object of the records to be inserted in the below format. The options parameter takes a object of optional parameters for the insertion. `insert` method also support upsert operations. See below: - -```json5 -{ - "records": [ - { - table: "string", //table into which record should be inserted - fields: { - column1: "value", //column names should match vault column names - ///... additional fields - } - }, - ///...additional records - ] -} -``` - -An example of an insert call is given below: - -```kt -//Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") -upsertArray.put(upsertColumn) -val insertOptions = Skyflow.InsertOptions(tokens= false,upsert= upsertArray) /*indicates whether or not tokens should be returned for the inserted data. Defaults to 'true'*/ -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback -val records = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "cards") -val fields = JSONObject() -fields.put("expiry_date", "12/2028") -fields.put("cardNumber", "41111111111") -record.put("fields", fields) -recordsArray.put(record) -records.put("records", recordsArray) -skyflowClient.insert(records = records, options = insertOptions, callback = insertCallback); -``` - -**Response :** -```json -{ - "records": [ - { - "table": "cards", - "fields":{ - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "expiry_date": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - } - ] -} -``` - ## Using Skyflow Elements to collect data **Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. @@ -519,203 +463,32 @@ fun clearFields(elements: List) { ``` -### Step 4 : Collect data from Elements -When the form is ready to be submitted, call the collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback) method on the container object. The options parameter takes `Skyflow.CollectOptions` object. +### Step 4: Collect data from Elements -`Skyflow.CollectOptions` takes two optional fields -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Inserting data into vault](#Inserting-data-into-the-vault) section. - -```kt -// NON-PCI fields object creation -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "persons") -val fields = JSONObject() -fields.put("gender", "MALE") -record.put("fields", fields) -recordsArray.put(record) -nonPCIRecords.put("records", recordsArray) - -val options = Skyflow.CollectOptions(tokens = true, additonalFields = nonPCIRecords) -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.callback -container.collect(options, insertCallback) -``` -### End to end example of collecting data with Skyflow Elements - -#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt): -```kt -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultId = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) - -//Initialize skyflow client -val skyflowClient = Skyflow.initialize(config) - -//Create a CollectContainer -val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) +Call `collect(callback, options)` on the container. `CollectOptions` accepts optional `additionalFields` (non-PCI data) and `upsert` parameters. -//Initialize and set required options -val options = Skyflow.CollectElementOptions(required = true) - -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val completedStyle = Skyflow.Style(textColor = Color.GREEN) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val inputStyles = Skyflow.Styles(base = baseStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create a CollectElementInput -val input = Skyflow.CollectElementInput( - table = "cards", - column = "cardNumber", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "card number", - placeholder = "card number", -) - -//Create a CollectElementOptions instance -val options = Skyflow.CollectElementOptions(required = true) - -//Create a Collect Element from the Collect Container -val skyflowElement = container.create(context = Context,input, options) - -//Can interact with this object as a normal UIView Object and add to View - -// Non-PCI fields data -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "persons") -val fields = JSONObject() -fields.put("gender", "MALE") -record.put("fields", fields) -recordsArray.put(record) -nonPCIRecords.put("records", recordsArray) - -//Initialize and set required options for insertion -val collectOptions = Skyflow.CollectOptions(tokens = true, additionalFields = nonPCIRecords) - -//Implement a custom Skyflow.Callback to be called on Insertion success/failure -public class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord(tableName = "persons", data = mapOf("gender" to "MALE")) + )) +) +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "success: ${record.tokens}") + else Log.d(TAG, "error [${record.httpCode}]: ${record.error}") + } } -} - -//Initialize InsertCallback which is an implementation of Skyflow.Callback interface -val insertCallback = InsertCallback() - -//Call collect method on CollectContainer -container.collect(options = collectOptions, callback = insertCallback) - -``` -#### Sample Response : -``` -{ - "records": [ - { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "failure: ${error.message}") } - ] -} - +}, options) ``` -### End to end example of upsert support with Skyflow Elements - -#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/UpsertFeature.kt): -```kt -val config = Skyflow.Configuration(vaultId = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) -val skyflowClient = Skyflow.initialize(config) -val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) -val options = Skyflow.CollectElementOptions(required = true) -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val completedStyle = Skyflow.Style(textColor = Color.GREEN) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val inputStyles = Skyflow.Styles(base = baseStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", - column = "card_number", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card number", - placeholder = "enter your card number", -) - -val nameInput = Skyflow.CollectElementInput( - table = "cards", - column = "full_name", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Full name", - placeholder = "enter your name", -) -val cardNumberElement = container.create(context = Context,cardNumberInput, options) -val nameElement = container.create(context = Context,namerInput, options) - -//Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") -upsertArray.put(upsertColumn) - -val collectOptions = Skyflow.CollectOptions(tokens = true,upsert = upsertArray) -public class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) - } -} - -val insertCallback = InsertCallback() -container.collect(options = collectOptions, callback = insertCallback) - -``` -#### Sample Response : -``` -{ - "records": [ - { - "table": "cards", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "name": "f3907186-e7e2-464f-91e5-48e12c2bfsi9" - } - } - ] -} +#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt) -``` ## Using Skyflow Elements to update data @@ -745,7 +518,7 @@ val collectElementInput = Skyflow.CollectElementInput( placeholder: String, // optional placeholder for the form element altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element - skyflowID: String // The skyflow_id of the record to be updated + skyflowId: String // The skyflow_id of the record to be updated ) ``` @@ -753,7 +526,6 @@ The `table` and `column` fields indicate which table and column in the vault the **Note:** - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object as described in the [collect section](#step-2-create-a-collect-element). @@ -785,175 +557,34 @@ fun clearFieldsOnSubmit(elements: List) { When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes an object of optional parameters as shown below: -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. - -```kt -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflow_id", "") // skyflowID for update - } - put("fields", fields) - } - recordsArray.put(record) - put("records", recordsArray) -} - -// Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") -} -upsertArray.put(upsertColumn) - -// Send the non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using `upsert` field of CollectOptions (optional) -val options = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertArray) - -// Custom callback - implementation of Skyflow.Callback -val insertCallback = InsertCallback() -container.collect(callback = insertCallback, options = options) -``` - -**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. - -### End to end example of updating data with Skyflow Elements - -```kt -// Initialize skyflow configuration -val tokenProvider = DemoTokenProvider() -val config = Configuration( - vaultID = "", - vaultURL = "", - tokenProvider = tokenProvider -) - -// Initialize skyflow client -val skyflowClient = init(config) - -// Create a CollectContainer -val container = skyflowClient.container(ContainerType.COLLECT) - -// Create Skyflow.Styles with individual Skyflow.Style variants -val padding = Padding(8, 8, 8, 8) -val baseStyle = Style(borderColor = Color.BLUE) -val baseTextStyle = Style(textColor = Color.BLACK) -val completeStyle = Style(borderColor = Color.GREEN) -val focusTextStyle = Style(textColor = Color.RED) -val inputStyles = Styles(base = baseStyle, complete = completeStyle) -val labelStyles = Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Styles(base = baseTextStyle) - -// Create a CollectElementInput with skyflowID for update -val input = CollectElementInput( - table = "cards", - column = "card_number", - type = SkyflowElementType.CARD_NUMBER, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card Number", - placeholder = "XXXX XXXX XXXX XXXX", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // skyflowID for update -) - -// Create an option to require the element -val requiredOption = CollectElementOptions(required = true, enableCopy = true) +- `additionalFields`: Non-PCI data to update or insert alongside element values, as an `AdditionalFields` object. +- `upsert`: To support upsert operations, pass a list of `UpsertOptions` specifying the table, update type, and unique columns. -// Create a Collect Element from the Collect Container -val skyflowElement = container.create(context = this, input = input, options = requiredOption) - -// Can interact with this object as a normal View Object and add to View -val parent = findViewById(R.id.parent) -parent.addView(skyflowElement) - -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - // Update existing person record - val personRecord = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "77dc3caf-c452-49e1-8625-07219d7567bf") // skyflowID for update - } - put("fields", fields) - } - // Update existing card record with additional fields - val cardRecord = JSONObject().apply { - put("table", "cards") - val fields = JSONObject().apply { - put("first_name", "Joe") - put("skyflowID", "431eaa6c-5c15-4513-aa15-29f50babe882") // same skyflowID as collect element +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord( + tableName = "persons", + data = mapOf("gender" to "MALE"), + skyflowId = "" + ) + )) +) +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "update success: ${record.tokens}") + else Log.d(TAG, "update error [${record.httpCode}]: ${record.error}") } - put("fields", fields) - } - recordsArray.put(personRecord) - recordsArray.put(cardRecord) - put("records", recordsArray) -} - -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") } - put(upsertColumn) -} - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using optional field `upsert` of CollectOptions -val collectOptions = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertOptions) - -// Implement a custom Skyflow.Callback to call on update success/failure -class InsertCallback : Callback { - override fun onSuccess(responseBody: Any) { - Log.d(TAG, "Update successful: $responseBody") - } - - override fun onFailure(exception: Any) { - Log.e(TAG, "Update failed: ${(exception as Exception).message}") + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "update failure: ${error.message}") } -} - -// Initialize custom Skyflow.Callback -val insertCallback = InsertCallback() - -// Call collect method on CollectContainer -container.collect(callback = insertCallback, options = collectOptions) +}, options) ``` -#### Skyflow returns tokens for the record you just updated: -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" - } - } - ] -} -``` +**Note:** `skyflowId` is required to update an existing record. Without it, a new record is inserted. ### Validations @@ -1442,190 +1073,29 @@ fun clearFieldsOnSubmit(elements: List) { ``` ### Step 4: Collect data from elements -When the form is ready to be submitted, call the `collect(options: Skyflow.CollectOptions? = CollectOptions(), callback: Skyflow.Callback)` method on the container object. The options parameter takes `Skyflow.CollectOptions` object. - -`Skyflow.CollectOptions` takes three optional fields -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Inserting data into vault](#Inserting-data-into-the-vault) section. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. - -```kotlin -// NON-PCI fields object creation -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() - -val record = JSONObject() -record.put("table", "persons") +When the form is ready to be submitted, call the `collect(callback: CollectCallback, options: CollectOptions? = null)` method on the container object. The options parameter takes a `CollectOptions` object. -val fields = JSONObject() -fields.put("gender", "MALE") - -record.put("fields", fields) -recordsArray.put(record) - -nonPCIRecords.put("records", recordsArray) - -//Upsert options -val upsertArray = JSONArray() - -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") - -upsertArray.put(upsertColumn) - -val options = Skyflow.CollectOptions(tokens = true, additonalFields = nonPCIRecords, upsert = upsertArray) -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.callback -container.collect(options, insertCallback) -``` -#### End to end example of collecting data with Composable Elements +`Skyflow.CollectOptions` takes two optional fields +- `additionalFields`: Non-PCI data to be inserted alongside element values. See [Additional fields](#additional-fields-non-pci-data). +- `upsert`: To support upsert operations, the table and a unique column. See [Upsert support](#upsert-support). -##### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/ComposableActivity.kt): ```kotlin -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) - -//Initialize skyflow client -val skyflowClient = Skyflow.init(config) - -//Create a ComposableContainer -val container = skyflowClient.container( - type = Skyflow.ContainerType.COMPOSABLE, - options = ContainerOptions(layout = arrayOf(1, 2)) -) - -//Initialize and set required options -val options = Skyflow.CollectElementOptions(required = true) - -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseCardStyle = Skyflow.Style(borderColor = Color.TRANSPARENT) -val baseDateStyle = Skyflow.Style(borderColor = Color.TRANSPARENT, width = 300) -val baseCvvStyle = Skyflow.Style(borderColor = Color.TRANSPARENT, width = 200) -val completedStyle = Skyflow.Style(textColor = Color.TRANSPARENT) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val cardStyles = Skyflow.Styles(base = baseCardStyle, complete = completedStyle) -val dateStyles = Skyflow.Styles(base = baseDateStyle, complete = completedStyle) -val cvvStyles = Skyflow.Styles(base = baseCvvStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create a CollectElementInput -val cardNumber = Skyflow.CollectElementInput( - table = "cards", - column = "cardNumber", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = cardStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "card number", - placeholder = "card number", -) - -val expDate = Skyflow.CollectElementInput( - table = "cards", - column = "expiryDate", - type = Skyflow.ElementType.EXPIRATION_DATE - inputStyles = dateStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Expiry Date", - placeholder = "mm/yy", -) - -val cvv = Skyflow.CollectElementInput( - table = "cards", - column = "cvv", - type = Skyflow.ElementType.CVV - inputStyles = cvvStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "CVV", - placeholder = "***", -) - -//Create a CollectElementOptions instance -val options = Skyflow.CollectElementOptions(required = true) - -//Create a Composable Element from the Composable Container -val cardNumberElement = container.create(context = Context, cardNumber, options) -val expDateElement = container.create(context = Context, expDate, options) -val cvvElement = container.create(context = Context, cvv, options) - -//Fetch composable layout and add to main view -try { - val composableLayout = container.getComposableLayout() - parent.addView(composableLayout) -} catch (error: Exception) { - println(error) -} - -// Non-PCI fields data -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() - -val record = JSONObject() -record.put("table", "persons") - -val fields = JSONObject() -fields.put("gender", "MALE") - -record.put("fields", fields) -recordsArray.put(record) - -nonPCIRecords.put("records", recordsArray) - -//Upsert options -val upsertArray = JSONArray() - -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") - -upsertArray.put(upsertColumn) - -//Initialize and set required options for insertion -val collectOptions = Skyflow.CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertArray) - -//Implement a custom Skyflow.Callback to be called on Insertion success/failure -class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) - } -} - -//Initialize InsertCallback which is an implementation of Skyflow.Callback interface -val insertCallback = InsertCallback() - -//Call collect method on CollectContainer -container.collect(options = collectOptions, callback = insertCallback) -``` -##### Sample Response : -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "expiryDate": "d0369871-91e5-466f-e7e2-48e12c2bcbc2", - "cvv": "c7093186-466f-e7e2-91e5-48e12c2bcbc3", - } - }, - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord(tableName = "persons", data = mapOf("gender" to "MALE")) + )) +) +composableContainer.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "success: ${record.tokens}") + else Log.d(TAG, "error [${record.httpCode}]: ${record.error}") + } } - ] -} + override fun onFailure(error: SkyflowError) { Log.d(TAG, "failure: ${error.message}") } +}, options) ``` -[For information on validations, see validations.](#validations) ## Using Skyflow Composable Elements to update data @@ -1663,7 +1133,7 @@ val composableElementInput = Skyflow.CollectElementInput( placeholder: String, // optional placeholder for the form element altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element - skyflowID: String // The skyflow_id of the record to be updated + skyflowId: String // The skyflow_id of the record to be updated ) ``` @@ -1715,313 +1185,96 @@ fun clearFieldsOnSubmit(elements: List) { ### Step 4: Update data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = null, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: CollectCallback, options: CollectOptions? = null)` method on the container object. -The options parameter takes a `Skyflow.CollectOptions` object as shown below: +The options parameter takes a `CollectOptions` object with the following optional fields: -- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. +- `additionalFields`: Non-PCI data to insert alongside element values, as an `AdditionalFields` object. +- `upsert`: To support upsert operations, pass a list of `UpsertOptions` specifying the table, update type, and unique columns. -```kt -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "") // skyflowID for update +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord( + tableName = "persons", + data = mapOf("gender" to "MALE"), + skyflowId = "" + ) + )) +) +composableContainer.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "update success: ${record.tokens}") + else Log.d(TAG, "update error [${record.httpCode}]: ${record.error}") } - put("fields", fields) } - recordsArray.put(record) - put("records", recordsArray) -} + override fun onFailure(error: SkyflowError) { Log.d(TAG, "update failure: ${error.message}") } +}, options) +``` -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") - } - put(upsertColumn) + +**Note:** `skyflowId` is required to update an existing record. Without it, a new record is inserted. + +## Event Listeners on Composable Elements +You can communicate with Skyflow Elements by listening to element events: + +```kotlin +element.on(eventName: Skyflow.EventName) { state -> + // handle function } +``` + +The SDK supports four events: + +- `CHANGE`: Triggered when the Element's value changes. +- `READY`: Triggered when the Element is fully rendered. +- `FOCUS`: Triggered when the Element gains focus. +- `BLUR`: Triggered when the Element loses focus. -// Send the non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using `upsert` field of CollectOptions (optional) -val options = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertOptions) +The handler `(state: JSONObject) -> Unit` is a callback function you provide, that will be called when the event is fired with the state object as shown below. -// Custom callback - implementation of Skyflow.Callback -val insertCallback = InsertCallback() -container.collect(callback = insertCallback, options = options) +```kotlin +val state = { + "elementType": Skyflow.ElementType, + "isEmpty": Bool , + "isRequired": Bool, + "isFocused": Bool, + "isValid": Bool, + "value": String +} ``` -### End to end example of updating data with Composable Elements +`Note`: +values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. -```kt -// Initialize skyflow configuration -val config = Configuration( - vaultID = "", - vaultURL = "", - tokenProvider = demoTokenProvider +#### Example Usage of Event Listener on Composable Elements + +```kotlin +// create skyflow client with loglevel:"DEBUG" +val config = Skyflow.Configuration( + vaultID = VAULT_ID, + vaultURL = VAULT_URL, + tokenProvider = demoTokenProvider, + options = Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) ) -// Initialize skyflow client -val skyflowClient = init(config) +val skyflowClient = Skyflow.init(config) -// Create container options with layout val containerOptions = ContainerOptions( - layout = arrayOf(1, 2), - styles = Styles(base = Style(borderColor = Color.GRAY)), - errorTextStyles = Styles(base = Style(textColor = Color.RED)) + layout = arrayOf(1, 1), + styles = Styles(base: Style(borderColor: UIColor.gray)), + errorTextStyles = Styles(base: Style(textColor: UIColor.red)) ) -// Create a Composable Container -val container = skyflowClient.container( - type = ContainerType.COMPOSABLE, - options = containerOptions -) - -// Create Skyflow.Styles with individual Skyflow.Style variants -val padding = Padding(8, 8, 8, 8) -val baseStyle = Style(borderColor = Color.BLUE) -val baseTextStyle = Style(textColor = Color.BLACK) -val completeStyle = Style(borderColor = Color.GREEN) -val focusTextStyle = Style(textColor = Color.RED) -val inputStyles = Styles(base = baseStyle, complete = completeStyle) -val labelStyles = Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Styles(base = baseTextStyle) - -// Create Composable Elements with skyflowID for update -val cardHolderNameElementInput = CollectElementInput( - table = "cards", - column = "cardholder_name", - type = SkyflowElementType.CARDHOLDER_NAME, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Cardholder Name", - placeholder = "John Doe", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // skyflowID for update -) - -// Create an option to require the element -val requiredOption = CollectElementOptions(required = true) - -// Create a Composable Element from the Composable Container -val cardHolderNameElement = container.create( - context = this, - input = cardHolderNameElementInput, - options = requiredOption -) - -val cardNumberElementInput = CollectElementInput( - table = "cards", - column = "card_number", - type = SkyflowElementType.CARD_NUMBER, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card Number", - placeholder = "XXXX XXXX XXXX XXXX", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // same skyflowID - will be merged in single update -) - -val cardNumberElement = container.create( - context = this, - input = cardNumberElementInput, - options = requiredOption -) - -val cvvElementInput = CollectElementInput( - table = "cards", - column = "cvv", - type = SkyflowElementType.CVV, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "CVV", - placeholder = "CVV", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // same skyflowID - will be merged in single update -) - -val cvvElement = container.create( - context = this, - input = cvvElementInput, - options = requiredOption -) - -// Add composable layout to screen -val parent = findViewById(R.id.parent) -try { - val composableLayout = container.getComposableLayout() - parent.addView(composableLayout) -} catch (error: Exception) { - println(error) -} - -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "77dc3caf-c452-49e1-8625-07219d7567bf") // skyflowID for update - } - put("fields", fields) - } - recordsArray.put(record) - put("records", recordsArray) -} - -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") - } - put(upsertColumn) -} - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using optional field `upsert` of CollectOptions -val collectOptions = CollectOptions( - tokens = true, - additionalFields = nonPCIRecords, - upsert = upsertOptions -) - -// Implement a custom Skyflow.Callback to call on update success/failure -class InsertCallback : Callback { - override fun onSuccess(responseBody: Any) { - Log.d(TAG, "Update successful: $responseBody") - } - - override fun onFailure(exception: Any) { - Log.e(TAG, "Update failed: ${(exception as Exception).message}") - } -} - -// Initialize custom Skyflow.Callback -val insertCallback = InsertCallback() - -// Call collect method on CollectContainer -container.collect(callback = insertCallback, options = collectOptions) -``` - -### Sample Success Response: - -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cardholder_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ] -} -``` - -### Sample Partial Error Response: - -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cardholder_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ], - "errors": [ - { - "error": { - "code": 400, - "description": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec", - "type": 400 - } - } - ] -} -``` - -## Event Listeners on Composable Elements -You can communicate with Skyflow Elements by listening to element events: - -```kotlin -element.on(eventName: Skyflow.EventName) { state -> - // handle function -} -``` - -The SDK supports four events: - -- `CHANGE`: Triggered when the Element's value changes. -- `READY`: Triggered when the Element is fully rendered. -- `FOCUS`: Triggered when the Element gains focus. -- `BLUR`: Triggered when the Element loses focus. - -The handler `(state: JSONObject) -> Unit` is a callback function you provide, that will be called when the event is fired with the state object as shown below. - -```kotlin -val state = { - "elementType": Skyflow.ElementType, - "isEmpty": Bool , - "isRequired": Bool, - "isFocused": Bool, - "isValid": Bool, - "value": String -} -``` - -`Note`: -values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format. - -#### Example Usage of Event Listener on Composable Elements - -```kotlin -// create skyflow client with loglevel:"DEBUG" -val config = Skyflow.Configuration( - vaultID = VAULT_ID, - vaultURL = VAULT_URL, - tokenProvider = demoTokenProvider, - options = Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) -) - -val skyflowClient = Skyflow.init(config) - -val containerOptions = ContainerOptions( - layout = arrayOf(1, 1), - styles = Styles(base: Style(borderColor: UIColor.gray)), - errorTextStyles = Styles(base: Style(textColor: UIColor.red)) -) - -//Create a Composable Container. -val container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) - -// Create a CollectElementInput -val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", - column = "cardNumber", - type = Skyflow.ElementType.CARD_NUMBER, +//Create a Composable Container. +val container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, options: containerOptions) + +// Create a CollectElementInput +val cardNumberInput = Skyflow.CollectElementInput( + table = "cards", + column = "cardNumber", + type = Skyflow.ElementType.CARD_NUMBER, ) val cardHolderNameInput = Skyflow.CollectElementInput( @@ -2228,275 +1481,11 @@ container.on(EventName.SUBMIT) { --- # Securely revealing data client-side -- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) - [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) - [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) - [**Set token for Reveal Elements**](#set-token-for-reveal-elements) - [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) -## Retrieving data from the vault -For non-PCI use-cases, retrieving data from the vault and revealing it in the mobile can be done either using the SkyflowID's or tokens as described below - -- ### Using tokens - To retrieve record data using tokens, use the `detokenize(records)` method. The `records` parameter takes a JSON object that contains tokens for record values to fetch: - - ```json5 - { - "records":[ - { - "token": "string", // token for the record to be fetched - "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED - } - ] - } - ``` - - Note: `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). - - The following example code makes a detokenize call to reveal the masked value of a token: - ```kt - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val records = JSONObject() - val recordsArray = JSONArray() - val recordObj = JSONObject() - recordObj.put("token", "45012507-f72b-4f5c-9bf9-86b133bae719") - recordObj.put("redaction", RedactionType.MASKED) - recordsArray.put(recordObj) - records.put("records", recordsArray) - - skyflowClient.detokenize(records = records, callback = getCallback) - ``` - The sample response: - ```json - { - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "j***oe" - } - ] - } - ``` - -- ### Using Skyflow ID's or Unique Column Values - For retrieving data from the vault, use the `get(records: JSONObject, options: GetOptions? = GetOptions(), callback: Skyflow.Callback)` method. - - The `records` parameter takes a JSON object that contains an array of the records to fetch. Each object inside array should contain: - - - Either an array of Skyflow IDs to fetch - - Or a column name and an array of column values - - The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. - - Notes: - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass GetOptions along with the redaction type. - - `tokens` defaults to false. - - ```json5 - { - "records":[ - { - "ids": JSONArray(), // Array of SkyflowID's of the records to be fetched - "table": String, // name of table holding the above skyflow_id's - "redaction": Skyflow.RedactionType // redaction to be applied to retrieved data - }, - { - "table": String, // name of table from where records are to be fetched - "redaction": Skyflow.RedactionType, // redaction to be applied to retrieved data - "columnName": String, // a unique column name - "colunmnValues": JSONArray() // Array of Column Values of the records to be fetched - } - ] - } - ``` - - An example of get call to fetch records: - ```kotlin - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val recordsArray = JSONArray() - - val record = JSONObject() - val skyflowIDs = JSONArray() - skyflowIDs.put("f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9") - skyflowIDs.put("da26de53-95d5-4bdb-99db-8d8c66a35ff9") - - record.put("ids", skyflowIDs) - record.put("table", "cards") - record.put("redaction", RedactionType.PLAIN_TEXT) - - val record1 = JSONObject() - val recordSkyflowIDs = JSONArray() - recordSkyflowIDs.put("invalid skyflow id") // invalid skyflow ID - - record1.put("ids", recordSkyflowIDs) - record1.put("table", "cards") - record1.put("redaction", RedactionType.PLAIN_TEXT) - - val record2 = JSONObject() - val columnValues = JSONArray() - columnValues.put("john.doe@gmail.com") - columnValues.put("jane.doe@gmail.com") - - record2.put("table", "customers") - record2.put("redaction", RedactionType.PLAIN_TEXT) - record2.put("columnName", "email") - record2.put("columnValues", columnValues) - - val record3 = JSONObject() - val columnValues1 = JSONArray() - columnValues1.put("invalid column value") // invalid column value - - record3.put("table", "customers") - record3.put("redaction", RedactionType.PLAIN_TEXT) - record3.put("columnName", "email") - record3.put("columnValues", columnValues1) - - recordsArray.put(record) - recordsArray.put(record1) - recordsArray.put(record2) - recordsArray.put(record3) - - val records = JSONObject() - records.put("records", recordsArray) - - skyflowClient.getById(records = records, GetOptions(), callback = getCallback) - ``` - - The sample response: - ```json - { - "records": [ - { - "fields": { - "card_number": "4111111111111111", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "4111111111111111", - "expiry_date": "10/23", - "fullname": "sam", - "id": "da26de53-95d5-4bdb-99db-8d8c66a35ff9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "4111111111111111", - "email": "john@doe@gmail.com", - "name": "john", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "customers" - }, - { - "fields": { - "card_number": "4111111111111111", - "email": "jane@doe@gmail.com", - "name": "jane", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "customers" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - }, - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "columnName": "customers", - "columnValues": ["invalid column value"] - } - ] - } - ``` - - An example of get call to fetch tokens: - ```kotlin - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val recordsArray = JSONArray() - - val validRecord = JSONObject() - val validSkyflowIDs = JSONArray() - validSkyflowIDs.put("f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9") - validSkyflowIDs.put("da26de53-95d5-4bdb-99db-8d8c66a35ff9") - - validRecord.put("ids", validSkyflowIDs) - validRecord.put("table", "cards") - - val invalidRecord = JSONObject() - val invalidRecordSkyflowIDs = JSONArray() - invalidRecordSkyflowIDs.put("invalid skyflow id") // invalid skyflow ID - - invalidRecord.put("ids", invalidRecordSkyflowIDs) - invalidRecord.put("table", "cards") - - recordsArray.put(validRecord) - recordsArray.put(invalidRecord) - - val records = JSONObject() - records.put("records", recordsArray) - - skyflowClient.getById(records = records, GetOptions(true), callback = getCallback) - ``` - - The sample Response: - ```json - { - "records": [ - { - "fields": { - "card_number": "9802-3257-3113-0294", - "expiry_date": "45012507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "131e2507-f72b-4f5c-9bf9-86b133bae719", - }, - "table": "cards" - }, - { - "fields": { - "card_number": "0294-3213-3157-9802", - "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - } - ] - } - ``` -### Redaction types - There are four enum values in Skyflow.RedactionType: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - ## Using Skyflow Elements to reveal data Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. ### Step 1: Create a container @@ -2506,23 +1495,21 @@ val container = skyflowClient.container(type = Skyflow.ContainerType.REVEAL) ``` ### Step 2: Create a reveal Element -Next, define a Skyflow Element to reveal data as shown below: -```kt -val revealElementInput = Skyflow.RevealElementInput( - token = "string", - redaction = Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED - inputStyles = Skyflow.Styles(), //optional, styles to be applied to the element - labelStyles = Skyflow.Styles(), //optional, styles to be applied to the label of the reveal element - errorTextStyles = Skyflow.Styles(), //optional styles that will be applied to the errorText of the reveal element - label = "cardNumber" //optional, label for the element, - altText = "XXXX XXXX XXXX XXXX" //optional, string that is shown before reveal, will show token if altText is not provided - ) +Next, define a `RevealElementInput` for each element to reveal: +```kotlin +val revealElementInput = RevealElementInput( + token = "", // token of the data to reveal + inputStyles = Skyflow.Styles(), // optional styles for the element + labelStyles = Skyflow.Styles(), // optional styles for the label + errorTextStyles = Skyflow.Styles(), // optional styles for the error text + label = "Card Number", // optional label + altText = "•••• •••• •••• ••••" // optional placeholder shown before reveal +) ``` -`Notes`: -- `token` is optional only if it is being used in invokeConnection() -- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types) + +**Note:** Redaction is not set on individual reveal elements. To apply a redaction to a token group, use `tokenGroupRedactions` on `RevealOptions` passed to `reveal()`. See [Reveal with typed callbacks](#reveal-with-typed-callbacks). The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. @@ -2603,12 +1590,38 @@ translation: hashmapOf('X' to "[0-9]") Elements used for revealing data are mounted to the screen the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-screen). ### Step 4: Reveal data -When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: -```kt -val revealCallback = RevealCallback() //Custom callback - implementation of Skyflow.Callback -container.reveal(callback = revealCallback) +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container with a typed `RevealCallback`: + +```kotlin +revealContainer.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "revealed: token=${record.token}, group=${record.tokenGroupName}") + } else { + Log.e(TAG, "partial error [${record.httpCode}]: ${record.error}") + } + } + } + override fun onFailure(error: SkyflowError) { + Log.e(TAG, "reveal failed: ${error.message}") + } +}) +``` + + +To apply redaction per token group, pass `RevealOptions`: + +```kotlin +val options = RevealOptions( + tokenGroupRedactions = listOf( + TokenGroupRedaction(tokenGroupName = "", redaction = "") + ) +) +revealContainer.reveal(object : RevealCallback { ... }, options) ``` + ### UI Error for Reveal Elements Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. @@ -2627,97 +1640,339 @@ The `setAltText(value: String)` method can be used to set the altText of the Rev ### End to end example of revealing data with Skyflow Elements #### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/RevealActivity.kt): -```kt -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultId = , vaultURL = , tokenProvider = demoTokenProvider) +```kotlin +// Initialize skyflow configuration +val config = Configuration( + vaultID = "", + vaultURL = "", + tokenProvider = demoTokenProvider +) -//Initialize skyflow client -val skyflowClient = Skyflow.initialize(config) +// Initialize skyflow client +val skyflowClient = init(config) -//Create a Reveal Container -val container = skyflowClient.container(type = Skyflow.ContainerType.REVEAL) +// Create a Reveal Container +val container = skyflowClient.container(ContainerType.REVEAL) +// Create Skyflow.Styles with individual Skyflow.Style variants +val baseStyle = Style(borderColor = Color.BLUE) +val baseTextStyle = Style(textColor = Color.BLACK) +val inputStyles = Styles(base = baseStyle) +val labelStyles = Styles(base = baseTextStyle) +val errorTextStyles = Styles(base = baseTextStyle) -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val inputStyles = Skyflow.Styles(base = baseStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create Reveal Elements -val cardNumberInput = Skyflow.RevealElementInput( - token = "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - redaction = RedactionType.MASKED, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "cardnumber", - altText = "XXXX XXXX XXXX XXXX" +// Create Reveal Elements — no redaction on individual elements +val cardNumberInput = RevealElementInput( + token = "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + inputStyles = inputStyles, + labelStyles = labelStyles, + errorTextStyles = errorTextStyles, + label = "Card Number", + altText = "XXXX XXXX XXXX XXXX" ) -val cardNumberElement = container.create(context = Context, input = cardNumberInput) +val cardNumberElement = container.create(context = this, input = cardNumberInput) -val nameInput = Skyflow.RevealElementInput( - token = "89024714-6a26-4256-b9d4-55ad69aa4047", - redaction = RedactionType.DEFAULT, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "fullname", - altText = "XXX" +val nameInput = RevealElementInput( + token = "89024714-6a26-4256-b9d4-55ad69aa4047", + inputStyles = inputStyles, + labelStyles = labelStyles, + errorTextStyles = errorTextStyles, + label = "Full Name", + altText = "XXX" ) -val nameElement = container.create(context = Context,input = nameInput) +val nameElement = container.create(context = this, input = nameInput) -//set error to the element +// Optionally set/reset custom error text on an element nameElement.setError("custom error") - -//reset error to the element nameElement.resetError() -//Can interact with these objects as a normal UIView Object and add to View +// Mount elements to the screen +parent.addView(cardNumberElement) +parent.addView(nameElement) + +// Call reveal with typed RevealCallback +container.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "revealed: token=${record.token}") + } else { + Log.e(TAG, "partial error [${record.httpCode}]: ${record.error}") + } + } + } + override fun onFailure(error: SkyflowError) { + Log.e(TAG, "reveal failed: ${error.message}") + } +}) +``` + +The `records` list contains both successful and failed tokens. Each record carries its own `httpCode` so you can handle mixed results in a single pass. + +#### Sample Response +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "error": "Detokenize failed. Token 89024714-6a26-4256-b9d4-55ad69aa4047 is invalid. Specify a valid token.", + "httpCode": 404 + } + ] +} +``` + +A whole-request failure (e.g. auth error) skips `onSuccess` and delivers a `SkyflowError` to `onFailure`: +```json +{ + "grpcCode": 13, + "httpCode": 500, + "message": "Skyflow services experienced an internal error.", + "httpStatus": "Internal Server Error", + "details": [] +} +``` +--- + +# Typed callbacks and response handling +The SDK provides typed callbacks and typed response objects for collect and reveal operations. Both successes and partial errors are returned in the same `records` list — each record carries its own `httpCode`, so you can handle mixed results without exceptions. -//Implement a custom Skyflow.Callback to be called on Reveal success/failure -public class RevealCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) +--- + +## Collect with typed callbacks + +### CollectCallback + +Implement `CollectCallback` to receive typed collect results: + +```kotlin +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "insert success: ${record.tokens}") + } else { + Log.d(TAG, "insert error [${record.httpCode}]: ${record.error}") + } + } } - override fun onFailure(exception: Exception) { - print(exception) + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "collect failure: code=${error.httpCode}, message=${error.message}") } -} +}) +``` + + +### CollectOptions + +#### Upsert support + +Pass `CollectOptions` with `upsert` to insert-or-update based on a unique column: + +```kotlin +val options = CollectOptions( + upsert = listOf( + UpsertOptions( + tableName = "", + updateType = UpdateType.UPDATE, + uniqueColumns = listOf("") + ) + ) +) +container.collect(object : CollectCallback { ... }, options) +``` + + +#### Additional fields (non-PCI data) + +Pass non-PCI data alongside element values using `AdditionalFields`: + +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields( + records = listOf( + AdditionalFieldsRecord( + tableName = "", + data = mapOf("" to "") + // skyflowId = "" // set this to update an existing record + ) + ) + ) +) +container.collect(object : CollectCallback { ... }, options) +``` -//Initialize custom Skyflow.Callback -val revealCallback = RevealCallback() -//Call reveal method on RevealContainer -container.reveal(callback = revealCallback) +**Note:** Set `skyflowId` on `AdditionalFieldsRecord` to update an existing record. Without it, a new record is inserted. +### CollectResponse + +`CollectResponse.records` is a flat list of `CollectRecord` objects. Both successes and partial errors are included in the same list. + +```kotlin +data class CollectRecord( + val tableName: String?, + val skyflowId: String?, + val tokens: Map?, + val hashedData: Map?, + val error: String?, + val httpCode: Int +) ``` -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. +Both success and partial-error records are delivered as `CollectRecord` in the same `records` list. Check `httpCode` on each record to distinguish them. -#### Sample Response:Callback +#### Sample response (success + partial error): ```json { - "success": [ - { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [ + {"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"} + ] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "error": "Invalid request. Required field ssn is missing.", + "skyflowId": null, + "httpCode": 400 + } + ] +} +``` + +The SDK populates `tableName` on error records from the original request, so `CollectRecord.tableName` is never empty in the delivered response. + +#### If the entire request fails, `onFailure` delivers a `SkyflowError`: + +```kotlin +override fun onFailure(error: SkyflowError) { + Log.d(TAG, "httpCode=${error.httpCode}, message=${error.message}") +} +``` + + +#### Sample Code: +[CollectActivity.kt](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt) + +--- + +## Reveal with typed callbacks + +### RevealCallback + +Implement `RevealCallback` to receive typed reveal results: + +```kotlin +revealContainer.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "reveal success: token=${record.token}") + } else { + Log.d(TAG, "reveal error [${record.httpCode}]: ${record.error}") + } + } + } + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "reveal failure: code=${error.httpCode}, message=${error.message}") } - ], - "errors": [ - { - "id": "89024714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" - } - } - ] +}) +``` + + +### RevealOptions + +Apply a redaction to an entire token group using `RevealOptions.tokenGroupRedactions`. This is a request-level setting — the redaction applies to every token in the named group, not to individual reveal elements. + +```kotlin +val options = RevealOptions( + tokenGroupRedactions = listOf( + TokenGroupRedaction( + tokenGroupName = "", + redaction = "" + ) + ) +) +revealContainer.reveal(object : RevealCallback { ... }, options) +``` + + +### RevealResponse + +`RevealResponse.records` is a flat list of `RevealRecord` objects. Both successes and partial errors are included in the same list. + +```kotlin +data class RevealRecord( + val token: String, + val tokenGroupName: String?, + val metadata: Map?, // includes skyflowId, tableName + val error: String?, + val httpCode: Int +) +``` + +#### Sample success response: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + } + ] } ``` + +#### Sample partial error response: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] +} +``` + +#### Sample Code: +[RevealActivity.kt](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/RevealActivity.kt) + +--- + ## Limitation Currently the skyflow collect elements and reveal elements can't be used in the XML layout definition, we have to add them to the views programatically. diff --git a/Skyflow/build.gradle b/Skyflow/build.gradle index 1a5ecef..cbff072 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/AdditionalFields.kt b/Skyflow/src/main/kotlin/Skyflow/AdditionalFields.kt new file mode 100644 index 0000000..d35fef7 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/AdditionalFields.kt @@ -0,0 +1,9 @@ +package Skyflow + +data class AdditionalFields(val records: List) + +data class AdditionalFieldsRecord( + val tableName: String, + val data: Map, + val skyflowId: String? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/Client.kt b/Skyflow/src/main/kotlin/Skyflow/Client.kt index d465822..2beffb1 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 a93115f..6574734 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 SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowInternalError(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)) + throw SkyflowInternalError(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 SkyflowInternalError(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 SkyflowInternalError(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 SkyflowInternalError(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 SkyflowInternalError(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 SkyflowInternalError(SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } else -> { val state = element.getState() @@ -107,45 +95,104 @@ 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 updateElements = collectElements.filter { !it.skyflowId.isNullOrEmpty() } + val insertElements = collectElements.filter { it.skyflowId.isNullOrEmpty() } + + val additionalUpdates = collectOptions.additionalFields?.records + ?.filter { !it.skyflowId.isNullOrEmpty() } ?: emptyList() + val additionalInserts = collectOptions.additionalFields?.records + ?.filter { it.skyflowId.isNullOrEmpty() } ?: emptyList() + + if (updateElements.isNotEmpty() || additionalUpdates.isNotEmpty()) { + // Build ONE combined update body — tableName per record so all go in a single API call + val updateRecordsArray = org.json.JSONArray() + + updateElements.groupBy { it.skyflowId }.forEach { (_, elements) -> + val tableName = elements.first().tableName + val skyflowID = elements.first().skyflowId!! + val singleBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowID, configuration.options.logLevel + ) + val rec = singleBody.getJSONArray("records").getJSONObject(0) + rec.put("tableName", tableName) + updateRecordsArray.put(rec) + } + + additionalUpdates.groupBy { it.skyflowId }.forEach { (_, records) -> + val merged = records.fold(mutableMapOf()) { acc, r -> acc.also { it.putAll(r.data) } } + val dataObj = org.json.JSONObject().apply { merged.forEach { (k, v) -> put(k, v) } } + updateRecordsArray.put( + org.json.JSONObject() + .put("skyflowID", records.first().skyflowId!!) + .put("data", dataObj) + .put("tableName", records.first().tableName) + ) + } + + val combinedUpdateBody = JSONObject() + .put("vaultID", configuration.vaultID) + .put("records", updateRecordsArray) + + val insertBody: JSONObject? = if (insertElements.isNotEmpty() || additionalInserts.isNotEmpty()) { + val insertOptions = CollectOptions( + upsert = collectOptions.upsert?.filter { opt -> insertElements.any { it.tableName == opt.tableName } }, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, + configuration.options.logLevel + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val insertOptions = CollectOptions( + upsert = collectOptions.upsert, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + insertOptions, 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 + this.client.apiClient.post(requestBody, callback, collectOptions) +} + +fun Container.collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(CollectResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) } - - 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, + } + collect(adapter, options) +} + +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 e2eef7d..65294f6 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt @@ -11,7 +11,7 @@ class CollectElementInput( internal var label: String = "", internal var placeholder: String = "", internal var validations: ValidationSet = ValidationSet(), - internal var skyflowID: String? = null + internal var skyflowId: String? = null ) { internal lateinit var type: SkyflowElementType @@ -33,7 +33,7 @@ class CollectElementInput( placeholder: String = "", altText: String = "", validations: ValidationSet = ValidationSet(), - skyflowID: String? = null + skyflowId: String? = null ) : this( table, column, @@ -43,9 +43,9 @@ class CollectElementInput( label, placeholder, validations, - skyflowID + skyflowId ) { 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 a07417c..f46d8cb 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt @@ -1,7 +1,6 @@ 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 +data class CollectOptions( + val upsert: List? = null, + val additionalFields: AdditionalFields? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt b/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt new file mode 100644 index 0000000..1f62707 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt @@ -0,0 +1,91 @@ +package Skyflow + +import org.json.JSONObject + +data class CollectRecord( + val tableName: String? = null, + val skyflowId: String? = null, + val error: String? = null, + val tokens: Map? = null, + val hashedData: Map? = null, + val httpCode: Int = 0 +) + +data class CollectResponse(val records: List = emptyList()) { + companion object { + fun fromJson(json: String): CollectResponse { + return try { + val root = JSONObject(json) + val records = root.optJSONArray("records")?.let { arr -> + (0 until arr.length()).mapNotNull { i -> + val r = arr.optJSONObject(i) ?: return@mapNotNull null + val httpCode = r.optInt("httpCode", 200) + if (r.has("error") && !r.isNull("error")) { + CollectRecord( + tableName = r.optString("tableName").ifEmpty { null }, + skyflowId = if (r.isNull("skyflowId")) null else r.optString("skyflowId").ifEmpty { null }, + error = r.optString("error"), + tokens = null, + hashedData = null, + httpCode = httpCode + ) + } else { + val fieldsObj = r.optJSONObject("tokens") + val tokens: Map? = fieldsObj?.let { obj -> + val map = mutableMapOf() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + val arr2 = obj.optJSONArray(key) + if (arr2 != null) { + map[key] = (0 until arr2.length()).mapNotNull { j -> + val e = arr2.optJSONObject(j) ?: return@mapNotNull null + e.keys().asSequence().associateWith { k -> e.opt(k) } + } + } else { + map[key] = obj.opt(key) + } + } + map + } + val hashedDataObj = r.optJSONObject("hashedData") + val hashedData: Map? = hashedDataObj?.let { obj -> + val map = mutableMapOf() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + val arr2 = obj.optJSONArray(key) + if (arr2 != null) { + map[key] = (0 until arr2.length()).mapNotNull { j -> + val e = arr2.optJSONObject(j) ?: return@mapNotNull null + e.keys().asSequence().associateWith { k -> e.opt(k) } + } + } else { + map[key] = obj.opt(key) + } + } + map + } + CollectRecord( + tableName = r.optString("tableName").ifEmpty { null }, + skyflowId = r.optString("skyflowId").ifEmpty { null }, + error = null, + tokens = tokens, + hashedData = hashedData, + httpCode = httpCode + ) + } + } + } ?: emptyList() + CollectResponse(records) + } catch (e: Exception) { + CollectResponse() + } + } + } +} + +interface CollectCallback { + fun onSuccess(response: CollectResponse) + fun onFailure(error: SkyflowError) +} diff --git a/Skyflow/src/main/kotlin/Skyflow/Configuration.kt b/Skyflow/src/main/kotlin/Skyflow/Configuration.kt index 9fb525a..36581ab 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 e120f27..236c348 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Element.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Element.kt @@ -13,7 +13,7 @@ open class Element @JvmOverloads constructor( internal var isRequired: Boolean = false internal var columnName: String = "" internal var tableName: String = "" - internal var skyflowID: String? = null + internal var skyflowId: String? = null internal lateinit var collectInput : CollectElementInput internal lateinit var options : Skyflow.CollectElementOptions internal lateinit var fieldType: SkyflowElementType @@ -35,9 +35,8 @@ 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 + skyflowId = this.collectInput.skyflowId state = State(columnName,isRequired) } diff --git a/Skyflow/src/main/kotlin/Skyflow/FlowDBRevealResponse.kt b/Skyflow/src/main/kotlin/Skyflow/FlowDBRevealResponse.kt new file mode 100644 index 0000000..e4a63cc --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/FlowDBRevealResponse.kt @@ -0,0 +1,56 @@ +package Skyflow + +import org.json.JSONObject + +data class RevealRecord( + val token: String, + val error: String? = null, + val tokenGroupName: String? = null, + val metadata: Map? = null, + val httpCode: Int = 0 +) + +data class RevealResponse(val records: List = emptyList()) { + companion object { + fun fromJson(json: String): RevealResponse { + return try { + val root = JSONObject(json) + val records = root.optJSONArray("records")?.let { arr -> + (0 until arr.length()).mapNotNull { i -> + val r = arr.optJSONObject(i) ?: return@mapNotNull null + val httpCode = r.optInt("httpCode", 200) + if (r.has("error") && !r.isNull("error")) { + RevealRecord( + token = r.optString("token"), + error = r.optString("error"), + tokenGroupName = null, + metadata = null, + httpCode = httpCode + ) + } else { + val metaObj = r.optJSONObject("metadata") + val metadata: Map? = metaObj?.let { obj -> + obj.keys().asSequence().associateWith { k -> obj.opt(k) } + } + RevealRecord( + token = r.optString("token"), + error = null, + tokenGroupName = r.optString("tokenGroupName").ifEmpty { null }, + metadata = metadata, + httpCode = httpCode + ) + } + } + } ?: emptyList() + RevealResponse(records) + } catch (e: Exception) { + RevealResponse() + } + } + } +} + +interface RevealCallback { + fun onSuccess(response: RevealResponse) + fun onFailure(error: SkyflowError) +} diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt b/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt index 5efb4e8..ef135a1 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, @@ -62,22 +61,22 @@ internal fun Container.validateElements() { for (element in this.revealElements) { val token = element.revealInput.token if (!checkIfElementsMounted(element)) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_NOT_MOUNTED_REVEAL, tag, configuration.options.logLevel, arrayOf(element.revealInput.label) ) } if (element.isTokenNull) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.TOKEN_KEY_NOT_FOUND_REVEAL, tag, configuration.options.logLevel, ) } else if (token!!.isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_TOKEN_REVEAL, tag, configuration.options.logLevel ) } else if (element.isError) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ERROR_STATE_REVEAL, tag, configuration.options.logLevel, arrayOf("${element.error.text}") ) @@ -85,12 +84,28 @@ internal fun Container.validateElements() { } } +fun Container.reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(RevealResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) + } + } + reveal(adapter, options) +} + internal fun Container.get(callback: Callback, options: RevealOptions?) { val revealValueCallback = RevealValueCallback( callback, 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, + options + ) + 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 1e132a1..14c173d 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt @@ -2,10 +2,9 @@ package Skyflow class RevealElementInput( internal var token: String? = null, - internal var redaction: RedactionType? = RedactionType.PLAIN_TEXT, 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/RevealOptions.kt b/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt index 9ae3cdb..90b8a1b 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt @@ -1,4 +1,10 @@ package Skyflow -class RevealOptions { -} \ No newline at end of file +data class TokenGroupRedaction( + val tokenGroupName: String, + val redaction: String +) + +data class RevealOptions( + val tokenGroupRedactions: List? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt b/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt index 2c763cf..4e83115 100644 --- a/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt +++ b/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt @@ -5,7 +5,7 @@ import Skyflow.utils.Utils -class SkyflowError(val skyflowErrorCode: SkyflowErrorCode = SkyflowErrorCode.UNKNOWN_ERROR, val tag : String? = "", logLevel: LogLevel? = null, params: Array = arrayOf()) : Exception(skyflowErrorCode.getMessage()) { +internal class SkyflowInternalError(val skyflowErrorCode: SkyflowErrorCode = SkyflowErrorCode.UNKNOWN_ERROR, val tag : String? = "", logLevel: LogLevel? = null, params: Array = arrayOf()) : Exception(skyflowErrorCode.getMessage()) { override var message = "" internal var internalMessage = "" diff --git a/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt b/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt new file mode 100644 index 0000000..73625db --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt @@ -0,0 +1,68 @@ +package Skyflow + +import org.json.JSONArray +import org.json.JSONObject + +data class SkyflowError( + val grpcCode: Int?, + val httpCode: Int?, + override val message: String?, + val httpStatus: String?, + val details: List? +) : Error(message) { + companion object { + fun fromJson(json: String): SkyflowError { + return try { + val root = JSONObject(json) + + // Priority 1: flat format { "grpcCode": 13, "httpCode": 500, "message": "...", ... } + if (root.has("grpcCode") || (root.has("httpCode") && !root.has("error") && !root.has("errors"))) { + return SkyflowError( + grpcCode = if (root.has("grpcCode")) root.optInt("grpcCode") else null, + httpCode = if (root.has("httpCode")) root.optInt("httpCode") else null, + message = root.optString("message").ifEmpty { null }, + httpStatus = root.optString("httpStatus").ifEmpty { null }, + details = parseDetails(root.optJSONArray("details")) + ) + } + + // Priority 2: wrapped format { "error": { "grpcCode", "httpCode", "message", ... } } + if (root.has("error") && root.optJSONObject("error") != null) { + val err = root.getJSONObject("error") + return SkyflowError( + grpcCode = if (err.has("grpcCode")) err.optInt("grpcCode") else null, + httpCode = err.optInt("httpCode", err.optInt("code", 500)), + message = err.optString("message", err.optString("description", "Unknown error")).ifEmpty { null }, + httpStatus = err.optString("httpStatus").ifEmpty { null }, + details = parseDetails(err.optJSONArray("details")) + ) + } + + // Priority 3: legacy format { "errors": [{ "error": { "code", "description" } }] } + val errorsArr = root.optJSONArray("errors") + if (errorsArr != null && errorsArr.length() > 0) { + val first = errorsArr.optJSONObject(0) + val err = first?.optJSONObject("error") ?: first + if (err != null) { + return SkyflowError( + grpcCode = null, + httpCode = err.optInt("code", err.optInt("httpCode", 500)), + message = err.optString("description", err.optString("message", "Unknown error")).ifEmpty { null }, + httpStatus = "${err.optInt("code", 500)}", + details = emptyList() + ) + } + } + + SkyflowError(null, 500, json, null, emptyList()) + } catch (e: Exception) { + SkyflowError(null, 500, e.message ?: "Unknown error", null, emptyList()) + } + } + + private fun parseDetails(arr: JSONArray?): List { + if (arr == null) return emptyList() + return (0 until arr.length()).mapNotNull { arr.opt(it) } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt b/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt new file mode 100644 index 0000000..57989bd --- /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 0000000..293eb9d --- /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/CollectAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt index 7c80d6b..3ef469f 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt @@ -61,26 +61,18 @@ internal class CollectAPICallback( val inputRecords = this.records["records"] as JSONArray val recordsArray = JSONArray() val responseObject = JSONObject() - if (this.options.tokens) { - for (i in responseJson.length() / 2 until responseJson.length()) { - val skyflowIDsObject = - JSONObject(responseJson[i - (responseJson.length() - responseJson.length() / 2)].toString()) - val skyflowIDs = skyflowIDsObject.getJSONArray("records") - val skyflowID = JSONObject(skyflowIDs[0].toString()).get("skyflow_id") - val record = JSONObject(responseJson[i].toString()) - val inputRecord = inputRecords.get(i - responseJson.length() / 2) as JSONObject - record.put("table", inputRecord["table"]) - val fields = JSONObject(record.get("fields").toString()) - fields.put("skyflow_id", skyflowID) - record.put("fields", fields) - recordsArray.put(record) - } - } else { - for (i in 0 until responseJson.length()) { - val inputRecord = inputRecords.get(i) as JSONObject - val record = (responseJson[i] as JSONObject)["records"] as JSONArray - recordsArray.put((record.get(0) as JSONObject).put("table", inputRecord["table"])) - } + for (i in responseJson.length() / 2 until responseJson.length()) { + val skyflowIDsObject = + JSONObject(responseJson[i - (responseJson.length() - responseJson.length() / 2)].toString()) + val skyflowIDs = skyflowIDsObject.getJSONArray("records") + val skyflowID = JSONObject(skyflowIDs[0].toString()).get("skyflow_id") + val record = JSONObject(responseJson[i].toString()) + val inputRecord = inputRecords.get(i - responseJson.length() / 2) as JSONObject + record.put("table", inputRecord["table"]) + val fields = JSONObject(record.get("fields").toString()) + fields.put("skyflow_id", skyflowID) + record.put("fields", fields) + recordsArray.put(record) } return responseObject.put("records", recordsArray) } diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt index 8c083cb..305cd2c 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt @@ -19,9 +19,9 @@ internal class CollectRequestBody { // Process elements first for (element in elements) { - if (element.skyflowID != null && element.skyflowID!!.isNotEmpty()) { + if (element.skyflowId != null && element.skyflowId!!.isNotEmpty()) { // This is an update record - val key = "${element.tableName}_${element.skyflowID}" + val key = "${element.tableName}_${element.skyflowId}" if (updateRecordsMap.containsKey(key)) { // Add column to existing update record updateRecordsMap[key]!!.columns[element.columnName] = element.getValue() @@ -30,7 +30,7 @@ internal class CollectRequestBody { val columns = mutableMapOf(element.columnName to element.getValue()) updateRecordsMap[key] = UpdateRequestRecord( table = element.tableName, - skyflowID = element.skyflowID!!, + skyflowID = element.skyflowId!!, columns = columns ) } @@ -120,7 +120,7 @@ internal class CollectRequestBody { } } if (!hasElementValueMatchRule) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, arrayOf(element.tableName, element.columnName) ) @@ -141,12 +141,12 @@ internal class CollectRequestBody { if (additionalFields != null) { if (additionalFields.has("records")) { if (additionalFields.get("records") !is JSONArray) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_INVALID_RECORDS, tag, logLevel ) val records = additionalFields.getJSONArray("records") if (records.length() == 0) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_RECORDS, tag, logLevel ) } @@ -154,28 +154,28 @@ internal class CollectRequestBody { while (i < records.length()) { val jsonobj = records.getJSONObject(i) if (!jsonobj.has("table")) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_TABLE_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) else if (!jsonobj.has("fields")) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_FIELDS_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) else if (jsonobj.getJSONObject("fields").toString() == "{}") - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_FIELDS, tag, logLevel, arrayOf("$i") ) val tableName = jsonobj.get("table") if (tableName !is String) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_INVALID_TABLE_NAME, tag, logLevel, arrayOf("$i") ) if (tableName.isEmpty()) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_TABLE_KEY, tag, logLevel, arrayOf("$i") ) @@ -185,7 +185,7 @@ internal class CollectRequestBody { val fieldList = mutableListOf() for (j in 0 until keys!!.length()) { if (keys.getString(j).isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, logLevel ) } @@ -197,7 +197,7 @@ internal class CollectRequestBody { if (tableMap[tableName] != null) { for (k in 0 until fieldList.size) { if (tableWithColumn.contains(tableName + fieldList[k].columnName)) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, arrayOf(tableName, fieldList[k].columnName) ) @@ -210,7 +210,7 @@ internal class CollectRequestBody { val tempArray = mutableListOf() for (k in 0 until fieldList.size) { if (tableWithColumn.contains(tableName + fieldList[k].columnName)) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, @@ -226,7 +226,7 @@ internal class CollectRequestBody { i++ } } else - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_RECORDS_KEY_NOT_FOUND, tag, logLevel ) } 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 0000000..585303f --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt @@ -0,0 +1,143 @@ +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 allRecords = JSONArray() + val requestRecords = requestBody.optJSONArray("records") + val topLevelTableName = requestBody.optString("tableName", "") + val requestTableNames: List = if (topLevelTableName.isNotEmpty()) { + // All records in this request belong to one table + List(requestRecords?.length() ?: 1) { topLevelTableName } + } else { + (0 until (requestRecords?.length() ?: 0)).map { i -> + requestRecords?.optJSONObject(i)?.optString("tableName", "") ?: "" + } + } + + for (i in 0 until records.length()) { + val record = records.getJSONObject(i) + val httpCode = record.optInt("httpCode", 200) + val fallbackTableName = requestTableNames.getOrElse(i) { "" } + val tableName = record.optString("tableName", "").ifEmpty { fallbackTableName } + + if (httpCode != 200) { + allRecords.put( + JSONObject() + .put("error", record.optString("error", "")) + .put("skyflowId", record.opt("skyflowID") ?: record.opt("skyflowId")) + .put("tableName", tableName) + .put("httpCode", httpCode) + ) + continue + } + + val skyflowId = record.optString("skyflowID", "").ifEmpty { record.optString("skyflowId", "") } + val fieldsObject = JSONObject() + + 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("tableName", tableName) + .put("skyflowId", skyflowId) + .put("tokens", fieldsObject) + .put("httpCode", httpCode) + val hashedData = record.optJSONObject("hashedData") + if (hashedData != null) resultRecord.put("hashedData", hashedData) + allRecords.put(resultRecord) + } + + callback.onSuccess(JSONObject().put("records", allRecords)) + } +} 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 0000000..312f8cd --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt @@ -0,0 +1,129 @@ +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() + + // Merge additionalFields insert records into tableMap + options.additionalFields?.records?.forEach { rec -> + val existing = tableMap.getOrPut(rec.tableName) { mutableListOf() } + rec.data.forEach { (k, v) -> existing.add(CollectRequestRecord(k, v.toString())) } + } + + 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))) + } + + internal fun buildUpdateRequestBodyFromMap( + vaultID: String, + tableName: String, + data: Map, + skyflowID: String + ): JSONObject { + val dataObject = JSONObject() + data.forEach { (k, v) -> dataObject.put(k, v) } + 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 SkyflowInternalError( + 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 0000000..ac39e04 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt @@ -0,0 +1,62 @@ +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 updateBody: JSONObject?, + private val insertBody: JSONObject?, + private val finalCallback: Callback, + private val options: CollectOptions, + val logLevel: LogLevel +) : Callback { + + private val totalCalls = (if (updateBody != null) 1 else 0) + (if (insertBody != null) 1 else 0) + private var completedCalls = 0 + private val allRecords = JSONArray() + + override fun onSuccess(responseBody: Any) { + val token = responseBody.toString() + updateBody?.let { body -> + 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) { + finalCallback.onFailure(exception) + } + } + + private fun mergeInto(target: JSONArray, source: JSONArray?) { + source ?: return + for (i in 0 until source.length()) target.put(source[i]) + } + + private fun dispatch() { + finalCallback.onSuccess(JSONObject().put("records", allRecords)) + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt index 8c3f9b5..dba8eb6 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt @@ -39,7 +39,7 @@ internal class UpdateAPICallback( if (e is SkyflowError) callback.onFailure(e) else { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = apiClient.logLevel, @@ -65,7 +65,7 @@ internal class UpdateAPICallback( val requestBody = JSONObject() requestBody.put("record", recordObject) - requestBody.put("tokenization", options.tokens) + requestBody.put("tokenization", true) val body: RequestBody = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) @@ -124,23 +124,17 @@ internal class UpdateAPICallback( val recordObject = JSONObject() recordObject.put("table", updateRecord.table) - if (options.tokens) { - val fieldsObject = JSONObject() - fieldsObject.put("skyflow_id", responseJson.getString("skyflow_id")) - - if (responseJson.has("tokens")) { - val tokens = responseJson.getJSONObject("tokens") - val tokenKeys = tokens.keys() - while (tokenKeys.hasNext()) { - val key = tokenKeys.next() - fieldsObject.put(key, tokens.getString(key)) - } + val fieldsObject = JSONObject() + fieldsObject.put("skyflow_id", responseJson.getString("skyflow_id")) + if (responseJson.has("tokens")) { + val tokens = responseJson.getJSONObject("tokens") + val tokenKeys = tokens.keys() + while (tokenKeys.hasNext()) { + val key = tokenKeys.next() + fieldsObject.put(key, tokens.getString(key)) } - - recordObject.put("fields", fieldsObject) - } else { - recordObject.put("skyflow_id", responseJson.getString("skyflow_id")) } + recordObject.put("fields", fieldsObject) responses.add(recordObject) } else { diff --git a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt index 35d2b10..815986b 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 @@ -75,7 +77,7 @@ fun Container.on(eventName: EventName, handler: (() -> Unit fun Container.getComposableLayout(): LinearLayout { if (collectElements.size != totalComposableElements) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, tag, configuration.options.logLevel @@ -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,13 +105,37 @@ fun Container.collect( } } +fun Container.collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(CollectResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) + } + } + collect(adapter, options) +} + +private fun Container.validateVaultConfig() { + if (configuration.vaultID.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowInternalError(SkyflowErrorCode.INVALID_VAULT_URL, tag, configuration.options.logLevel) + } +} + private fun Container.validateElements() { var errors = "" for (element in this.collectElements) { errors = validateElement(element, errors) } if (errors != "") { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.INVALID_INPUT, tag, configuration.options.logLevel, arrayOf(errors) ) @@ -122,7 +148,7 @@ private fun Container.validateElement( ): String { var errorOnElement = err if (!element.isAttachedToWindow) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, configuration.options.logLevel, @@ -131,7 +157,7 @@ private fun Container.validateElement( } when { element.collectInput.table.equals(null) -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, tag, configuration.options.logLevel, @@ -139,7 +165,7 @@ private fun Container.validateElement( ) } element.collectInput.column.equals(null) -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.MISSING_COLUMN, tag, configuration.options.logLevel, @@ -147,7 +173,7 @@ private fun Container.validateElement( ) } element.collectInput.table!!.isEmpty() -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, configuration.options.logLevel, @@ -155,7 +181,7 @@ private fun Container.validateElement( ) } element.collectInput.column!!.isEmpty() -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, configuration.options.logLevel, @@ -175,43 +201,87 @@ 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 updateElements = collectElements.filter { !it.skyflowId.isNullOrEmpty() } + val insertElements = collectElements.filter { it.skyflowId.isNullOrEmpty() } + + val additionalUpdates = collectOptions.additionalFields?.records + ?.filter { !it.skyflowId.isNullOrEmpty() } ?: emptyList() + val additionalInserts = collectOptions.additionalFields?.records + ?.filter { it.skyflowId.isNullOrEmpty() } ?: emptyList() + + if (updateElements.isNotEmpty() || additionalUpdates.isNotEmpty()) { + val updateRecordsArray = org.json.JSONArray() + + updateElements.groupBy { it.skyflowId }.forEach { (_, elements) -> + val tableName = elements.first().tableName + val skyflowID = elements.first().skyflowId!! + val singleBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowID, configuration.options.logLevel + ) + val rec = singleBody.getJSONArray("records").getJSONObject(0) + rec.put("tableName", tableName) + updateRecordsArray.put(rec) + } + + additionalUpdates.groupBy { it.skyflowId }.forEach { (_, records) -> + val merged = records.fold(mutableMapOf()) { acc, r -> acc.also { it.putAll(r.data) } } + val dataObj = org.json.JSONObject().apply { merged.forEach { (k, v) -> put(k, v) } } + updateRecordsArray.put( + org.json.JSONObject() + .put("skyflowID", records.first().skyflowId!!) + .put("data", dataObj) + .put("tableName", records.first().tableName) + ) + } + + val combinedUpdateBody = JSONObject() + .put("vaultID", configuration.vaultID) + .put("records", updateRecordsArray) + + val insertBody: JSONObject? = if (insertElements.isNotEmpty() || additionalInserts.isNotEmpty()) { + val insertOptions = CollectOptions( + upsert = collectOptions.upsert?.filter { opt -> insertElements.any { it.tableName == opt.tableName } }, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, + configuration.options.logLevel + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val insertOptions = CollectOptions( + upsert = collectOptions.upsert, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + insertOptions, 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/APIClient.kt b/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt index f3b1e46..b1c213d 100644 --- a/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt +++ b/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt @@ -26,7 +26,6 @@ object JWTUtils { val split = JWTEncoded.split(".").toTypedArray() JSONObject(getJson(split[1])) } catch (e: UnsupportedEncodingException) { - println(e.toString()) JSONObject() } } @@ -70,7 +69,7 @@ internal class APIClient( Logger.info(tag, Messages.BEARER_TOKEN_RECEIVED.getMessage(), logLevel) if (!isValidToken(responseBody.toString())) { val error = - SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) + SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) callback.onFailure(error) } else { token = "Bearer $responseBody" @@ -85,7 +84,7 @@ internal class APIClient( logLevel ) val error = - SkyflowError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel) + SkyflowInternalError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel) callback.onFailure(error) } }) @@ -93,7 +92,7 @@ internal class APIClient( callback.onSuccess(token) } } catch (e: Exception) { - val error = SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) callback.onFailure(error) } } @@ -181,13 +180,13 @@ internal class APIClient( fun constructBodyForDetokenize(records: JSONObject): MutableList { if (!records.has("records")) { - throw SkyflowError(SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, tag, logLevel) } if (records.get("records") !is JSONArray) { - throw SkyflowError(SkyflowErrorCode.INVALID_RECORDS, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_RECORDS, tag, logLevel) } if (records.getJSONArray("records").length() == 0) { - throw SkyflowError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) } val jsonArray = records.getJSONArray("records") val list = mutableListOf() @@ -195,21 +194,21 @@ internal class APIClient( while (i < jsonArray.length()) { val recordObject = jsonArray.getJSONObject(i) if (!recordObject.has("token")) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.TOKEN_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) } else if (recordObject.get("token").toString().isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_TOKEN, tag, logLevel, arrayOf("$i") ) } else if (recordObject.has("redaction")) { val redaction = recordObject.get("redaction") if (redaction.toString().isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_REDACTION_VALUE, tag, logLevel, arrayOf("$i") ) } else if (redaction !is RedactionType) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.INVALID_REDACTION_TYPE, tag, logLevel, arrayOf("$i") ) } else { diff --git a/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt b/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt index 8481263..b89f3bd 100644 --- a/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt @@ -73,7 +73,7 @@ internal class ConnectionApiCallback( sendRequest(requestBuild) } catch (e:Exception){ - callback.onFailure(Utils.constructError(SkyflowError(SkyflowErrorCode.UNKNOWN_ERROR, tag, logLevel, params = arrayOf(e.message)))) + callback.onFailure(Utils.constructError(SkyflowInternalError(SkyflowErrorCode.UNKNOWN_ERROR, tag, logLevel, params = arrayOf(e.message)))) } } override fun onFailure(exception: Any) { @@ -88,7 +88,7 @@ internal class ConnectionApiCallback( else tokens = error.getString("token") } - callback.onFailure(Utils.constructError(SkyflowError(SkyflowErrorCode.NOT_VALID_TOKENS, tag, logLevel, params = arrayOf(tokens)))) + callback.onFailure(Utils.constructError(SkyflowInternalError(SkyflowErrorCode.NOT_VALID_TOKENS, tag, logLevel, params = arrayOf(tokens)))) } catch (e:Exception){ callback.onFailure(exception) @@ -139,7 +139,7 @@ internal class ConnectionApiCallback( fun getRequestBuild(responseBody: Any, requestBody: JSONObject): Request? { //create requestBuild val requestUrlBuilder = connectionUrl.toHttpUrlOrNull()?.newBuilder() if(requestUrlBuilder == null){ - val error = SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL, + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_CONNECTION_URL, tag, logLevel, arrayOf(connectionConfig.connectionURL)) callback.onFailure(Utils.constructError(error)) return null @@ -203,13 +203,13 @@ internal class ConnectionApiCallback( } else { - val skyflowError = SkyflowError(SkyflowErrorCode.BAD_REQUEST, tag = tag, logLevel = logLevel) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.BAD_REQUEST, tag = tag, logLevel = logLevel) callback.onFailure(Utils.constructError(skyflowError, response.code)) } } catch (e:Exception) { - val skyflowError = SkyflowError(SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = logLevel, arrayOf(e.message.toString())) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = logLevel, arrayOf(e.message.toString())) skyflowError.setErrorCode(400) callback.onFailure(Utils.constructError(skyflowError, response.code)) } @@ -223,23 +223,23 @@ internal class ConnectionApiCallback( for (j in 0 until keys.length()) { if(keys.getString(j).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_BODY, + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_BODY, tag, logLevel) } var value: Any if (records.get(keys.getString(j)) is Element) { val element = (records.get(keys.getString(j)) as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (records.get(keys.getString(j)) as Element).getValue() } else if (records.get(keys.getString(j)) is Label) { if(!Utils.checkIfElementsMounted(records.get(keys.getString(j)) as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((records.get(keys.getString(j)) as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN_IN_CONNECTION_REQUEST, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN_IN_CONNECTION_REQUEST, tag, logLevel, arrayOf(keys.getString(j))) else if ((records.get(keys.getString(j)) as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) value = Utils.getValueForLabel(records.get(keys.getString(j)) as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } else if (records.get(keys.getString(j)) is JSONObject) { constructRequestBodyForConnection(records.get(keys.getString(j)) as JSONObject) @@ -258,18 +258,18 @@ internal class ConnectionApiCallback( { val element = (arrayValue.get(k) as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (arrayValue.get(k) as Element).getValue() } else if(arrayValue.get(k) is Label) { if(!Utils.checkIfElementsMounted(arrayValue.get(k) as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((arrayValue.get(k) as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) else if ((arrayValue.get(k) as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) value = Utils.getValueForLabel(arrayValue.get(k) as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } else if(arrayValue.get(k) is JSONObject) @@ -280,7 +280,7 @@ internal class ConnectionApiCallback( else if(arrayValue.get(k) is String || arrayValue.get(k) is Number || arrayValue.get(k) is Boolean) value = arrayValue.get(k) .toString() else - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) arrayValue.put(k,value) } value = arrayValue @@ -294,7 +294,7 @@ internal class ConnectionApiCallback( { val element = (arrayValue[k] as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (arrayValue[k] as Element).getValue() } @@ -302,11 +302,11 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(arrayValue[k] as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((arrayValue[k] as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) else if ((arrayValue[k] as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) else value = Utils.getValueForLabel(arrayValue[k] as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } @@ -318,7 +318,7 @@ internal class ConnectionApiCallback( else if(arrayValue[k] is String || arrayValue[k] is Number || arrayValue[k] is Boolean) value = arrayValue[k].toString() else - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) arrayInRequestBody.put(k,value) } value = arrayInRequestBody @@ -327,7 +327,7 @@ internal class ConnectionApiCallback( else if (records.get(keys.getString(j)) is String || records.get(keys.getString(j)) is Number || records.get(keys.getString(j)) is Boolean) value = records.get(keys.getString(j)).toString() else { - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) } records.put(keys.getString(j), value) } } @@ -342,7 +342,7 @@ internal class ConnectionApiCallback( if(headers.getString(i).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_HEADER_PARAMS) //empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_HEADER_PARAMS) //empty key } if(headers.getString(i).lowercase(Locale.getDefault()).equals("content-type")) { if(connectionConfig.requestHeader.get(headers.getString(i)) is ContentType) { @@ -358,7 +358,7 @@ internal class ConnectionApiCallback( else { //callback.onFailure(Exception("invalid field \"${headers.getString(i)}\" present in requestHeader")) - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_HEADER_PARAMS, tag, logLevel, arrayOf(headers.getString(i))) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_HEADER_PARAMS, tag, logLevel, arrayOf(headers.getString(i))) throw skyflowError } } @@ -371,7 +371,7 @@ internal class ConnectionApiCallback( for (i in 0 until queryParams.length()) { if(queryParams.getString(i).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_QUERY_PARAMS)//empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_QUERY_PARAMS)//empty key } val value = connectionConfig.queryParams.get(queryParams.getString(i)) if(value is Array<*>) @@ -396,7 +396,7 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(value)) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) } checkForValidElement(value) queryMap.put(key, value.getValue()) @@ -405,13 +405,13 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(value)) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) } else if (value.isTokenNull) { - val error = SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) throw error } else if (value.revealInput.token!!.isEmpty()) { - val error = SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) throw error } queryMap.put(key,Utils.getValueForLabel(value,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel)) @@ -422,7 +422,7 @@ internal class ConnectionApiCallback( } else { //callback.onFailure(Exception("invalid field \"${key}\" present in queryParams")) - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_QUERY_PARAMS, tag, logLevel, arrayOf(key)) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_QUERY_PARAMS, tag, logLevel, arrayOf(key)) throw skyflowError } } @@ -436,14 +436,14 @@ internal class ConnectionApiCallback( for (j in 0 until keys.length()) { if(keys.getString(j).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_PATH_PARAMS, tag, logLevel) //empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_PATH_PARAMS, tag, logLevel) //empty key } var value = params.get(keys.getString(j)) if (value is Element) { val element = value if(!Utils.checkIfElementsMounted(element)) { - val error = SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + val error = SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) throw error } checkForValidElement(element) @@ -451,14 +451,14 @@ internal class ConnectionApiCallback( } else if (value is Label) { if(!Utils.checkIfElementsMounted(value)) { - val error = SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + val error = SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) throw error } else if (value.isTokenNull) { - val error = SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) throw error } else if (value.revealInput.token!!.isEmpty()) { - val error = SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) throw error } else @@ -468,7 +468,7 @@ internal class ConnectionApiCallback( value = value.toString() newURL = newURL.replace("{" + keys.getString(j) + "}", value) } else { - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_PATH_PARAMS, tag, logLevel, arrayOf(keys.getString(j))) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_PATH_PARAMS, tag, logLevel, arrayOf(keys.getString(j))) throw skyflowError } } @@ -492,7 +492,7 @@ internal class ConnectionApiCallback( errors = "for " + labelName + " " + (state["validationError"] as String) + "\n" } if (errors != "") { - val error = SkyflowError(SkyflowErrorCode.INVALID_INPUT, tag, logLevel, arrayOf(errors)) + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_INPUT, tag, logLevel, arrayOf(errors)) throw error } } @@ -510,13 +510,13 @@ internal class ConnectionApiCallback( { val element = (responseBody.get(keys.getString(j))) as Element if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) } else if(responseBody.get(keys.getString(j)) is Label) { val element = (responseBody.get(keys.getString(j))) as Label if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) } else if (responseBody.get(keys.getString(j)) is JSONObject) { validateResponseBody(responseBody.get(keys.getString(j)) as JSONObject, elementList) @@ -524,7 +524,7 @@ internal class ConnectionApiCallback( throw Exception("invalid field " + keys.getString(j) + " present in response body") if (responseBody.get(keys.getString(j)) is Element || responseBody.get(keys.getString(j)) is Label) { if (elementList.contains(responseBody.get(keys.getString(j)).hashCode().toString())) - throw SkyflowError(SkyflowErrorCode.DUPLICATE_ELEMENT_FOUND, Utils.tag,logLevel) + throw SkyflowInternalError(SkyflowErrorCode.DUPLICATE_ELEMENT_FOUND, Utils.tag,logLevel) else elementList.add(responseBody.get(keys.getString(j)).hashCode().toString()) } @@ -577,7 +577,7 @@ internal class ConnectionApiCallback( if(e is SkyflowError) throw e else - throw SkyflowError(SkyflowErrorCode.NOT_FOUND_IN_RESPONSE, + throw SkyflowInternalError(SkyflowErrorCode.NOT_FOUND_IN_RESPONSE, Utils.tag, logLevel, arrayOf(keys.getString(j))) } } } 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 0000000..0ea1cd6 --- /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(SkyflowInternalError(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(SkyflowInternalError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel)) + } + }) + } else { + callback.onSuccess(token) + } + } catch (e: Exception) { + callback.onFailure(SkyflowInternalError(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 SkyflowInternalError(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/get/GetAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt index 5667cbb..707b6b7 100644 --- a/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt @@ -3,6 +3,7 @@ package Skyflow.get import Skyflow.Callback import Skyflow.SkyflowError import Skyflow.SkyflowErrorCode +import Skyflow.SkyflowInternalError import Skyflow.core.APIClient import Skyflow.utils.Utils import okhttp3.Call @@ -39,7 +40,7 @@ internal class GetAPICallback( private fun buildRequest(responseBody: Any, record: GetRecord): Request { val url = "${apiClient.vaultURL}${apiClient.vaultId}/${record.table}" val requestUrlBuilder = url.toHttpUrlOrNull()?.newBuilder() - ?: throw SkyflowError( + ?: throw SkyflowInternalError( SkyflowErrorCode.INVALID_VAULT_URL, tag, apiClient.logLevel, arrayOf(apiClient.vaultURL) ) @@ -74,7 +75,7 @@ internal class GetAPICallback( private fun sendRequest(request: Request, record: GetRecord) { okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { override fun onFailure(call: Call, e: IOException) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = apiClient.logLevel, params = arrayOf(e.message.toString()) ) @@ -96,7 +97,7 @@ internal class GetAPICallback( try { val responseErrorBody = JSONObject(responseBody) val requestId = response.headers["x-request-id"].toString() - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.SERVER_ERROR, tag, apiClient.logLevel, arrayOf( Utils.appendRequestId( @@ -109,7 +110,7 @@ internal class GetAPICallback( val responseObject = constructErrorResponseForGet(record, skyflowError) getResponse.insertResponse(JSONArray().put(responseObject), false) } catch (e: Exception) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.SERVER_ERROR, tag, apiClient.logLevel, arrayOf(responseBody) ) @@ -132,14 +133,14 @@ internal class GetAPICallback( } getResponse.insertResponse(newJsonArray, true) } else { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.BAD_REQUEST, tag, apiClient.logLevel ) val responseObject = constructErrorResponseForGet(record, skyflowError) getResponse.insertResponse(JSONArray().put(responseObject), false) } } catch (e: Exception) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag, apiClient.logLevel, arrayOf(e.message.toString()) ) @@ -152,7 +153,7 @@ internal class GetAPICallback( private fun constructErrorResponseForGet( record: GetRecord, - skyflowError: SkyflowError + skyflowError: SkyflowInternalError ): JSONObject { val responseObject = JSONObject() responseObject.put("error", skyflowError) diff --git a/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt b/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt index 22bc0c7..5e59e36 100644 --- a/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt +++ b/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt @@ -4,6 +4,7 @@ import Skyflow.Callback import Skyflow.LogLevel import Skyflow.SkyflowError import Skyflow.SkyflowErrorCode +import Skyflow.SkyflowInternalError import Skyflow.utils.Utils import org.json.JSONArray import org.json.JSONObject @@ -39,7 +40,7 @@ internal class GetResponse( if (successResponses + failureResponses + emptyResponses == size) { if (successResponses + failureResponses == 0) { - val skyflowError = SkyflowError(SkyflowErrorCode.FAILED_TO_GET, tag, logLevel) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.FAILED_TO_GET, tag, logLevel) callback.onFailure(Utils.constructError(skyflowError)) } else { if (failureResponses == 0) { 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 0000000..e62e11c --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt @@ -0,0 +1,118 @@ +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 allRecords = 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) { + allRecords.put( + JSONObject() + .put("token", originalToken) + .put("value", entry.optString("value")) + .put("tokenGroupName", entry.optString("tokenGroupName")) + .put("httpCode", httpCode) + .put("metadata", entry.optJSONObject("metadata")) + ) + } else { + allRecords.put( + JSONObject() + .put("token", originalToken) + .put("error", entry.optString("error", "Detokenize failed")) + .put("httpCode", httpCode) + ) + } + } + + callback.onSuccess(JSONObject().put("records", allRecords)) + } +} 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 0000000..8222fc4 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt @@ -0,0 +1,41 @@ +package Skyflow.reveal + +import Skyflow.Label +import Skyflow.RevealOptions +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBRevealRequestBody { + companion object { + internal fun buildRequestBody( + vaultID: String, + elements: MutableList