From 138c46b036224dcb46ff61c5bb7ba2ef5434f95e Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 22 Jul 2026 14:30:08 +0530 Subject: [PATCH 01/14] SK-3005 flowdb support --- .../Skyflow/collect/CollectContainer.swift | 6 +- Sources/Skyflow/collect/CollectOptions.swift | 4 +- .../collect/FlowVaultCollectAPICallback.swift | 292 +++++++++ .../collect/FlowVaultCollectRequestBody.swift | 197 ++++++ .../Skyflow/collect/FlowVaultICOptions.swift | 48 ++ .../collect/FlowVaultInsertAPICallback.swift | 280 ++++++++ .../collect/FlowVaultInsertRequestBody.swift | 29 + .../collect/FlowVaultUpdateRequestBody.swift | 13 + Sources/Skyflow/collect/InsertOptions.swift | 4 +- Sources/Skyflow/collect/UpsertOption.swift | 24 + .../composable/ComposableContainer.swift | 6 +- Sources/Skyflow/core/APIClient.swift | 22 +- Sources/Skyflow/core/Client.swift | 49 +- Sources/Skyflow/core/InterfaceName.swift | 4 +- Sources/Skyflow/core/Message.swift | 2 +- Sources/Skyflow/errors/ErrorCodes.swift | 16 +- .../FlowVaultDetokenizeRequestBody.swift | 18 + .../reveal/FlowVaultRevealApiCallback.swift | 154 +++++ .../Skyflow/reveal/RevealApiCallback.swift | 26 +- Sources/Skyflow/reveal/RevealContainer.swift | 8 +- .../Skyflow/reveal/RevealElementInput.swift | 12 - Sources/Skyflow/reveal/RevealOptions.swift | 8 +- .../Skyflow/reveal/RevealRequestBody.swift | 1 - .../Skyflow/reveal/RevealRequestRecord.swift | 4 +- .../Skyflow/reveal/TokenGroupRedaction.swift | 17 + Sources/Skyflow/skyflow_iOS.swift | 7 - .../skyflow_iOS_collectTests.swift | 95 +-- ...skyflow_iOS_composableEelementsTests.swift | 4 +- .../skyflow_iOS_elementTests.swift | 2 +- .../Skyflow_iOS_collectErrorTests.swift | 109 +-- .../Skyflow_iOS_generalErrorTests.swift | 8 - .../Skyflow_iOS_revealErrorTests.swift | 94 +-- .../skyflow_iOS_revealTests.swift | 29 +- .../ClientScenario.swift | 2 +- .../skyflow_iOS_collectUtilTests.swift | 620 ++++++++---------- .../skyflow_iOS_insertUtilTests 2.swift | 197 ++++-- .../skyflow_iOS_revealUtilTests.swift | 160 ++--- .../skyflow_iOS_utilTests.swift | 38 +- 38 files changed, 1688 insertions(+), 921 deletions(-) create mode 100644 Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift create mode 100644 Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift create mode 100644 Sources/Skyflow/collect/FlowVaultICOptions.swift create mode 100644 Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift create mode 100644 Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift create mode 100644 Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift create mode 100644 Sources/Skyflow/collect/UpsertOption.swift create mode 100644 Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift create mode 100644 Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift create mode 100644 Sources/Skyflow/reveal/TokenGroupRedaction.swift delete mode 100644 Sources/Skyflow/skyflow_iOS.swift diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainer.swift index 16bb9035..07ac2707 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainer.swift @@ -29,7 +29,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -84,8 +84,8 @@ public extension Container { return } } - let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; diff --git a/Sources/Skyflow/collect/CollectOptions.swift b/Sources/Skyflow/collect/CollectOptions.swift index 369961bb..6814cb45 100644 --- a/Sources/Skyflow/collect/CollectOptions.swift +++ b/Sources/Skyflow/collect/CollectOptions.swift @@ -9,8 +9,8 @@ import Foundation public struct CollectOptions { var tokens: Bool var additionalFields: [String: Any]? - var upsert: [[String: Any]]? - public init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [[String: Any]]? = nil) { + var upsert: [UpsertOption]? + public init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil) { self.tokens = tokens self.additionalFields = additionalFields self.upsert = upsert diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift new file mode 100644 index 00000000..9bcb1c42 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultCollectAPICallback: Callback { + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updatesByTable = self.groupUpdatesByTable(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updatesByTable.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": [], "errors": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including passing a full-request-level failure straight through, unwrapped). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + self.callback.onFailure(response) + return + } + let errors = response["errors"] as? [[String: Any]] ?? [] + if errors.isEmpty { + self.callback.onSuccess(response) + } else { + self.callback.onFailure(response) + } + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + for (tableName, tableRecords) in updatesByTable { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, tableName: tableName, records: tableRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + } + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + let response: [String: Any] = ["records": mergedRecords, "errors": mergedErrors] + if mergedErrors.isEmpty { + self.callback.onSuccess(response) + } else { + self.callback.onFailure(response) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func groupUpdatesByTable(_ updateDict: [String: Any]) -> [String: [[String: Any]]] { + var updatesByTable: [String: [[String: Any]]] = [:] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + let record: [String: Any] = ["skyflowID": skyflowID, "data": fields] + updatesByTable[tableName, default: []].append(record) + } + return updatesByTable + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + + } + + internal func getUpdateRequestSession(url: URL, tableName: String, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, tableName: tableName, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + do { + let errorResponse = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] + if let errorDetails = errorResponse["error"] as? [String: Any], + let message = errorDetails["message"] as? String { + description = message + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } catch { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "code": httpResponse.statusCode]] + } + } + return ["error": ["message": description, "code": httpResponse.statusCode]] + } + } + + guard let safeData = data else { + return ["records": [], "errors": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + var successRecords: [[String: Any]] = [] + var errorRecords: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + errorRecords.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + if let data = entry["data"] as? [String: Any] { successEntry["data"] = self.buildFieldsDict(dict: data) } + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { + successEntry["tokens"] = self.buildFieldsDict(dict: tokens) + } + successRecords.append(successEntry) + } + } + + return ["records": successRecords, "errors": errorRecords] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift new file mode 100644 index 00000000..bd1a231e --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Class for formatting the request body for collecting the data (FlowDB v2) + +import Foundation + +internal class FlowVaultCollectRequestBody { + static var tableSet: Set = Set() + static var callback: Callback? + static var breakFlag = false + static var mergedDict: [String: Any] = [:] + + internal static func addFieldsToTableSet(tableName: String, prefix: String, fields: [String: Any], contextOptions: ContextOptions) { + if !self.breakFlag { + for (key, val) in fields { + if val is [String: Any] { + addFieldsToTableSet(tableName: tableName, prefix: prefix == "" ? key : prefix + "." + key, fields: val as! [String: Any], contextOptions: contextOptions) + } else { + let tableSetEntry = tableName + "-" + (prefix == "" ? key : prefix + "." + key) + if tableSet.contains(tableSetEntry) { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } else { + self.tableSet.insert(tableSetEntry) + } + } + } + } + } + + internal static func mergeFields(tableName: String, prefix: String, dict: [String: Any], contextOptions: ContextOptions) { + for(key, val) in dict { + let keypath = prefix == "" ? key : prefix + "." + key + if val is [String: Any] { + mergeFields(tableName: tableName, prefix: keypath, dict: val as! [String: Any], contextOptions: contextOptions) + } else { + if mergedDict[keyPath: keypath] == nil { + mergedDict[keyPath: keypath] = val + } else { + if !self.breakFlag { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: key).getErrorObject(contextOptions: contextOptions)) + self.breakFlag = true + return + } + } + } + } + } + + internal static func createRequestBody( + elements: [TextField], + additionalFields: [String: Any]? = nil, + callback: Callback, + contextOptions: ContextOptions + ) -> [String: Any]? { + var tableMap: [String: Int] = [:] + var payload: [[String: Any]] = [] + var updatePayload: [String: Any] = [:] + self.callback = callback + self.breakFlag = false + self.tableSet = Set() + var index: Int = 0 + var inputPayload: [[String: Any]] = [] + + if additionalFields != nil { + inputPayload = additionalFields?["records"] as! [[String: Any]] + for entry in inputPayload { + let entryDict = entry + let tableName = entryDict["table"] as! String + let fields = entryDict["fields"] as! [String: Any] + if entryDict["skyflowID"] != nil { + let skyflowID = entryDict["skyflowID"] as! String + if updatePayload[skyflowID] != nil { + let temp = updatePayload[skyflowID] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + for (key, val) in fields { + existingFields[key] = val + } + var updatedTemp = temp + updatedTemp["fields"] = existingFields + updatePayload[skyflowID] = updatedTemp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": fields, + "skyflowID": skyflowID + ] + updatePayload[skyflowID] = temp + } + continue + } + if tableMap[tableName] != nil { + let inputEntry = payload[tableMap[tableName]!] + mergedDict = inputEntry["fields"] as! [String: Any] + self.mergeFields(tableName: tableName, prefix: "", dict: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload[tableMap[tableName]!]["fields"] = mergedDict + mergedDict = [:] + } else { + tableMap[tableName] = index + let temp: [String: Any] = [ + "table": tableName, + "fields": fields + ] + self.addFieldsToTableSet(tableName: tableName, prefix: "", fields: fields, contextOptions: contextOptions) + if self.breakFlag { + return nil + } + payload.append(temp) + index += 1 + } + } + } + + for element in elements { + let tableName = element.tableName! + let columnName = element.columnName! + let value = element.getValue() + let skyflowID = element.skyflowID // Assumes TextField has this property + + if let skyflowID = skyflowID, !skyflowID.isEmpty { + if updatePayload[skyflowID] != nil { + var temp = updatePayload[skyflowID] as! [String: Any] + var existingFields = temp["fields"] as! [String: Any] + if existingFields[columnName] != nil { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } else { + existingFields[columnName] = value + } + temp["fields"] = existingFields + updatePayload[skyflowID] = temp + } else { + let temp: [String: Any] = [ + "table": tableName, + "fields": [columnName: value], + "skyflowID": skyflowID + ] + updatePayload[skyflowID] = temp + } + } else { + // Only add to payload + if tableMap[(element.tableName)!] != nil { + var temp = payload[tableMap[(element.tableName)!]!] + temp[keyPath: "fields." + (element.columnName)!] = element.getValue() + let tableSetEntry = element.tableName! + "-" + element.columnName + if tableSet.contains(tableSetEntry) { + var hasElementValueMatchRule: Bool = false + for validation in element.userValidationRules.rules { + if validation is ElementValueMatchRule { + hasElementValueMatchRule = true + break; + } + } + if(!hasElementValueMatchRule) + { + self.callback?.onFailure(ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: [ element.columnName, element.tableName!]).getErrorObject(contextOptions: contextOptions)) + return nil + } + continue; + } + self.tableSet.insert(tableSetEntry) + payload[tableMap[(element.tableName)!]!] = temp + } else { + tableMap[(element.tableName)!] = index + index += 1 + var temp: [String: Any] = [ + "table": element.tableName!, + "fields": [:] + ] + temp[keyPath: "fields." + element.columnName!] = element.getValue() + self.tableSet.insert(element.tableName! + "-" + element.columnName) + payload.append(temp) + } + } + } + return ["records": payload, "update": updatePayload] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultICOptions.swift b/Sources/Skyflow/collect/FlowVaultICOptions.swift new file mode 100644 index 00000000..c11b4463 --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultICOptions.swift @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Options passed internally for FlowDB v2 collect/insert calls + +import Foundation + +internal struct FlowVaultICOptions { + var tokens: Bool + var additionalFields: [String: Any]? + var upsert: [UpsertOption]? + var callback: Callback? + var contextOptions: ContextOptions? + + init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { + self.tokens = tokens + self.additionalFields = additionalFields + self.upsert = upsert + self.callback = callback + self.contextOptions = contextOptions + } + + public func validateUpsert() -> Bool{ + if self.upsert != nil { + if self.upsert!.count == 0 { + let errorCode = ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY() + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + + for (index, currUpsertOption) in self.upsert!.enumerated() { + if currUpsertOption.table == "" { + let errorCode = ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + if currUpsertOption.uniqueColumns.isEmpty { + let errorCode = ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "\(index)") + self.callback!.onFailure(errorCode.getErrorObject(contextOptions: self.contextOptions!)) + return true + } + } + + } + return false; + } +} diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift new file mode 100644 index 00000000..df88e01a --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Callback used while API callback for Collect the elements (FlowDB v2) + +import Foundation +import UIKit + +internal class FlowVaultInsertAPICallback: Callback { + var apiClient: APIClient + var records: [String: Any] + var callback: Callback + var options: FlowVaultICOptions + var contextOptions: ContextOptions + + internal init(callback: Callback, apiClient: APIClient, records: [String: Any], options: FlowVaultICOptions, contextOptions: ContextOptions) { + self.records = records + self.apiClient = apiClient + self.callback = callback + self.options = options + self.contextOptions = contextOptions + } + internal func onSuccess(_ responseBody: Any) { + let insertRecords = records["records"] as? [[String: Any]] ?? [] + let updatesByTable = self.groupUpdatesByTable(records["update"] as? [String: Any] ?? [:]) + let hasInsert = !insertRecords.isEmpty + let hasUpdate = !updatesByTable.isEmpty + + if !hasInsert && !hasUpdate { + self.callback.onSuccess(["records": [], "errors": []]) + return + } + + // No update records: preserve the original single-call insert behavior exactly + // (including throwing a full-request-level failure straight to onFailure). + if !hasUpdate { + guard let url = URL(string: self.apiClient.vaultURL + "v2/records/insert") else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + let errors = response["errors"] as? [[String: Any]] ?? [] + if errors.isEmpty { + self.callback.onSuccess(response) + } else { + self.callback.onFailure(response) + } + } catch { + self.callback.onFailure(error) + } + } + task.resume() + } catch let error { + self.callback.onFailure(error) + } + return + } + + guard URL(string: self.apiClient.vaultURL + "v2/records/insert") != nil else { + self.callback.onFailure(ErrorCodes.INVALID_URL().getErrorObject(contextOptions: self.contextOptions)) + return + } + + let group = DispatchGroup() + var mergedRecords: [[String: Any]] = [] + var mergedErrors: [[String: Any]] = [] + + if hasInsert { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/insert")! + do { + let (request, session) = try self.getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + for (tableName, tableRecords) in updatesByTable { + group.enter() + let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! + do { + let (request, session) = try self.getUpdateRequestSession(url: url, tableName: tableName, records: tableRecords) + let task = session.dataTask(with: request) { data, response, error in + defer { group.leave() } + do { + let response = try self.processResponse(data: data, response: response, error: error) + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + } catch { + mergedErrors.append(["error": error.localizedDescription]) + } + } + task.resume() + } catch let error { + mergedErrors.append(["error": error.localizedDescription]) + group.leave() + } + } + + group.notify(queue: .main) { + let response: [String: Any] = ["records": mergedRecords, "errors": mergedErrors] + if mergedErrors.isEmpty { + self.callback.onSuccess(response) + } else { + self.callback.onFailure(response) + } + } + } + + internal func onFailure(_ error: Any) { + self.callback.onFailure(error) + } + + internal func groupUpdatesByTable(_ updateDict: [String: Any]) -> [String: [[String: Any]]] { + var updatesByTable: [String: [[String: Any]]] = [:] + for (skyflowID, value) in updateDict { + guard let entry = value as? [String: Any], + let tableName = entry["table"] as? String, + let fields = entry["fields"] as? [String: Any] else { continue } + let record: [String: Any] = ["skyflowID": skyflowID, "data": fields] + updatesByTable[tableName, default: []].append(record) + } + return updatesByTable + } + + internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { + var temp: [String: Any] = [:] + for (key, val) in dict { + if let v = val as? [String: Any] { + temp[key] = buildFieldsDict(dict: v) + } else { + temp[key] = val + } + } + return temp + } + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultInsertRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: self.records, options: options)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + + } + + internal func getUpdateRequestSession(url: URL, tableName: String, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, tableName: tableName, records: records)) + request.httpBody = data + } + + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + throw error! + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every record in the batch fails, + // but the body still has the same {"records": [...]} shape as a success/partial + // response (each record carrying its own error/httpCode) - parse it as such. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["records"] != nil { + return try getCollectResponseBody(data: safeData) + } + var description = "Insert call failed with the following status code" + String(httpResponse.statusCode) + var errorObject: Error = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) + + if let safeData = data { + do { + let desc = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] + let error = desc["error"] as! [String: Any] + description = error["message"] as! String + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) + } catch { + errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8)!).getErrorObject(contextOptions: self.contextOptions) + } + } + throw errorObject + } + } + + guard let safeData = data else { + return ["records": [], "errors": []] + } + + return try getCollectResponseBody(data: safeData) + + } + + func getCollectResponseBody(data: Data) throws -> [String: Any]{ + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + var successRecords: [[String: Any]] = [] + var errorRecords: [[String: Any]] = [] + + let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + errorRecords.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + if let data = entry["data"] as? [String: Any] { successEntry["data"] = self.buildFieldsDict(dict: data) } + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { + successEntry["tokens"] = self.buildFieldsDict(dict: tokens) + } + successRecords.append(successEntry) + } + } + + return ["records": successRecords, "errors": errorRecords] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift b/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift new file mode 100644 index 00000000..e2ef86ff --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultInsertRequestBody.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 insert api call + +import Foundation + +internal class FlowVaultInsertRequestBody { + internal static func createRequestBody(vaultID: String, records: [String: Any], options: FlowVaultICOptions) -> [String: Any] { + var recordsPayload: [[String: Any]] = [] + for record in (records["records"] as! [[String: Any]]) { + var temp: [String: Any] = [:] + temp["data"] = record["fields"] + if let tableName = record["table"] as? String { + temp["tableName"] = tableName + if let upsertOptions = options.upsert, let match = upsertOptions.first(where: { $0.table == tableName }) { + var upsertPayload: [String: Any] = ["uniqueColumns": match.uniqueColumns] + if let updateType = match.updateType { + upsertPayload["updateType"] = updateType.rawValue + } + temp["upsert"] = upsertPayload + } + } + recordsPayload.append(temp) + } + return ["vaultID": vaultID, "records": recordsPayload] + } +} diff --git a/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift new file mode 100644 index 00000000..ad929c5f --- /dev/null +++ b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 update-by-skyflowID api call + +import Foundation + +internal class FlowVaultUpdateRequestBody { + internal static func createRequestBody(vaultID: String, tableName: String, records: [[String: Any]]) -> [String: Any] { + return ["vaultID": vaultID, "tableName": tableName, "records": records] + } +} diff --git a/Sources/Skyflow/collect/InsertOptions.swift b/Sources/Skyflow/collect/InsertOptions.swift index e57737df..6fc5725b 100644 --- a/Sources/Skyflow/collect/InsertOptions.swift +++ b/Sources/Skyflow/collect/InsertOptions.swift @@ -8,8 +8,8 @@ import Foundation public struct InsertOptions { var tokens: Bool - var upsert: [[String: Any]]? - public init(tokens: Bool = true, upsert: [[String: Any]]? = nil) { + var upsert: [UpsertOption]? + public init(tokens: Bool = true, upsert: [UpsertOption]? = nil) { self.tokens = tokens self.upsert = upsert } diff --git a/Sources/Skyflow/collect/UpsertOption.swift b/Sources/Skyflow/collect/UpsertOption.swift new file mode 100644 index 00000000..18011787 --- /dev/null +++ b/Sources/Skyflow/collect/UpsertOption.swift @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes an upsert configuration for insert/collect calls + +import Foundation + +public enum UpdateType: String { + case UPDATE + case REPLACE +} + +public struct UpsertOption { + public let table: String + public let uniqueColumns: [String] + public let updateType: UpdateType? + + public init(table: String, uniqueColumns: [String], updateType: UpdateType? = nil) { + self.table = table + self.uniqueColumns = uniqueColumns + self.updateType = updateType + } +} diff --git a/Sources/Skyflow/composable/ComposableContainer.swift b/Sources/Skyflow/composable/ComposableContainer.swift index 5da7784c..4e35b746 100644 --- a/Sources/Skyflow/composable/ComposableContainer.swift +++ b/Sources/Skyflow/composable/ComposableContainer.swift @@ -211,7 +211,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -268,8 +268,8 @@ public extension Container { return } } - let records = CollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = ICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; diff --git a/Sources/Skyflow/core/APIClient.swift b/Sources/Skyflow/core/APIClient.swift index 3cfd6ef9..f5c19512 100644 --- a/Sources/Skyflow/core/APIClient.swift +++ b/Sources/Skyflow/core/APIClient.swift @@ -59,12 +59,12 @@ internal class APIClient { } } - internal func postAndUpdate(records: [String: Any], callback: Callback, options: ICOptions, contextOptions: ContextOptions) { - let collectApiCallback = CollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func postAndUpdate(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let collectApiCallback = FlowVaultCollectAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) self.getAccessToken(callback: collectApiCallback, contextOptions: contextOptions) } - internal func post(records: [String: Any], callback: Callback, options: ICOptions, contextOptions: ContextOptions) { - let insertApiCallback = InsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) + internal func post(records: [String: Any], callback: Callback, options: FlowVaultICOptions, contextOptions: ContextOptions) { + let insertApiCallback = FlowVaultInsertAPICallback(callback: callback, apiClient: self, records: records, options: options, contextOptions: contextOptions) self.getAccessToken(callback: insertApiCallback, contextOptions: contextOptions) } internal func constructUpdateRequestBody(records: [String: Any], options: ICOptions) -> [String: Any] { @@ -85,7 +85,7 @@ internal class APIClient { temp["tableName"] = record["table"] temp["method"] = "POST" temp["quorum"] = true - + if options.tokens { var temp2: [String: Any] = [:] temp2["method"] = "GET" @@ -114,18 +114,18 @@ internal class APIClient { } return uniqueColumn; } - - internal func get(records: [RevealRequestRecord], callback: Callback, contextOptions: ContextOptions) { - let revealApiCallback = RevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) + + internal func get(records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, callback: Callback, contextOptions: ContextOptions) { + let revealApiCallback = FlowVaultRevealAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v2/tokens/detokenize"), records: records, tokenGroupRedactions: tokenGroupRedactions, contextOptions: contextOptions) self.getAccessToken(callback: revealApiCallback, contextOptions: contextOptions) } internal func getById(records: [GetByIdRecord], callback: Callback, contextOptions: ContextOptions) { - let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, contextOptions: contextOptions) + let revealByIdApiCallback = RevealByIDAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v1/vaults/" + vaultID), records: records, contextOptions: contextOptions) self.getAccessToken(callback: revealByIdApiCallback, contextOptions: contextOptions) } - internal func getRecord(records: [GetRecord], callback: Callback,getOptions: GetOptions, contextOptions: ContextOptions) { - let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + vaultID), records: records, getOptions: getOptions,contextOptions: contextOptions) + internal func getRecord(records: [GetRecord], callback: Callback, getOptions: GetOptions, contextOptions: ContextOptions) { + let getApiCallback = GetAPICallback(callback: callback, apiClient: self, connectionUrl: (vaultURL + "v1/vaults/" + vaultID), records: records, getOptions: getOptions, contextOptions: contextOptions) self.getAccessToken(callback: getApiCallback, contextOptions: contextOptions) } } diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index c9e51761..81c1a379 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -15,8 +15,9 @@ public class Client { public init(_ skyflowConfig: Configuration) { self.vaultID = skyflowConfig.vaultID - self.vaultURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL + "v1/vaults/" : skyflowConfig.vaultURL + "/v1/vaults/" - self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: self.vaultURL, tokenProvider: skyflowConfig.tokenProvider) + let normalizedBaseURL = skyflowConfig.vaultURL.hasSuffix("/") ? skyflowConfig.vaultURL : skyflowConfig.vaultURL + "/" + self.vaultURL = normalizedBaseURL + "v2/vaults/" + self.apiClient = APIClient(vaultID: skyflowConfig.vaultID, vaultURL: normalizedBaseURL, tokenProvider: skyflowConfig.tokenProvider) self.contextOptions = ContextOptions(logLevel: skyflowConfig.options!.logLevel, env: skyflowConfig.options!.env, interface: .CLIENT) Log.info(message: .CLIENT_INITIALIZED, contextOptions: self.contextOptions) } @@ -29,11 +30,11 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - let icOptions = ICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) var errorCode: ErrorCodes? if records["records"] == nil { @@ -118,11 +119,6 @@ public class Client { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { - if token["redaction"] != nil { - guard let _ = token["redaction"] as? RedactionType else { - return .INVALID_REDACTION_TYPE() - } - } if token["token"] == nil { return .ID_KEY_ERROR() } else { @@ -138,7 +134,7 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -156,11 +152,7 @@ public class Client { for (index,token) in tokens.enumerated() { let errorCode = checkRecord(token: token, index: index) if errorCode == nil, let id = token["token"] as? String { - if token["redaction"] == nil{ - list.append(RevealRequestRecord(token: id, redaction: RedactionType.PLAIN_TEXT.rawValue)) - } else if let redaction = token["redaction"] as? RedactionType{ - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) - } + list.append(RevealRequestRecord(token: id)) } else { return callRevealOnFailure(callback: callback, errorObject: errorCode!.getErrorObject(contextOptions: tempContextOptions)) } @@ -172,7 +164,7 @@ public class Client { onFailureHandler: { } ) - self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } else { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } @@ -186,7 +178,7 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -269,12 +261,12 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.vaultURL == "/v1/vaults/" { + if self.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callRevealOnFailure(callback: callback, errorObject: errorCode.getErrorObject(contextOptions: tempContextOptions)) } Log.info(message: .VALIDATE_GET_INPUT, contextOptions: tempContextOptions) - + if records["records"] == nil { return callRevealOnFailure(callback: callback, errorObject: ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: tempContextOptions)) } @@ -305,7 +297,7 @@ public class Client { let logCallback = LogCallback(clientCallback: callback, contextOptions: tempContextOptions, onSuccessHandler: { Log.info(message: .GET_SUCCESS, contextOptions: tempContextOptions) }, onFailureHandler: { - + }) self.apiClient.getRecord(records: list, callback: logCallback, getOptions: options, contextOptions: tempContextOptions) } else { @@ -343,7 +335,7 @@ public class Client { if( getOptions.tokens == true ){ if (entry["columnName"] != nil || entry["columnValues"] != nil){ return .TOKENS_GET_COLUMN_NOT_SUPPPORTED() - + } if (entry["redaction"] as? RedactionType) != nil { return .REDACTION_WITH_TOKEN_NOT_SUPPORTED() @@ -355,7 +347,7 @@ public class Client { return .INVALID_REDACTION_TYPE() } } - + if(entry["columnName"] == nil){ if ((entry["ids"] == nil) && (entry["columnValues"] == nil)){ return .MISSING_IDS_OR_COLUMN_VALUES_IN_GET() @@ -389,7 +381,7 @@ public class Client { return .EMPTY_COLUMN_NAME() } } - + return nil } @@ -397,17 +389,6 @@ public class Client { let result = ["errors": [errorObject]] callback.onFailure(result) } - - internal func createDetokenizeRecords(_ IDsToTokens: [String: String]) -> [String: [[String: String]]]{ - var records = [] as [[String : String]] - var index = 0 - for (_, token) in IDsToTokens { - records.append(["token": token]) - index += 1 - } - - return ["records": records] - } } internal class LogCallback: Callback { diff --git a/Sources/Skyflow/core/InterfaceName.swift b/Sources/Skyflow/core/InterfaceName.swift index 54534af7..4afd2dee 100644 --- a/Sources/Skyflow/core/InterfaceName.swift +++ b/Sources/Skyflow/core/InterfaceName.swift @@ -4,7 +4,7 @@ // // File.swift -// +// // // Created by Akhil Anil Mangala on 29/11/21. // @@ -21,7 +21,7 @@ internal enum InterfaceName { case GETBYID case GET case EMPTY - + var description: String { switch self { case .COLLECT_CONTAINER: return "collect container" diff --git a/Sources/Skyflow/core/Message.swift b/Sources/Skyflow/core/Message.swift index e7c7b918..33796934 100644 --- a/Sources/Skyflow/core/Message.swift +++ b/Sources/Skyflow/core/Message.swift @@ -76,7 +76,7 @@ internal enum Message { case .INSERT_TRIGGERED: return "Insert method triggered." case .DETOKENIZE_TRIGGERED: return "Detokenize method triggered." case .GET_BY_ID_TRIGGERED: return "Get by ID triggered." - + //Used in tests case .CLIENT_CONNECTION: return "client connection not established" // A case .CANNOT_CHANGE_ELEMENT: return "Element can't be changed" // A diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/Skyflow/errors/ErrorCodes.swift index d117f4bc..67bb66e1 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/Skyflow/errors/ErrorCodes.swift @@ -30,7 +30,6 @@ internal enum ErrorCodes: CustomStringConvertible { case INVALID_FIELDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error.invaid 'fields' key value in record at index . Specify a value of type array for 'fields' key.", value: String) case INVALID_RECORDS_TYPE(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'records' key found. Specify a value of type array instead.") case INVALID_BEARER_TOKEN_FORMAT(code: Int = 400, message: String = "\(LangAndVersion) Token generated from 'getBearerToken' callback function is invalid. Make sure the implementation of 'getBearerToken' is correct.") - case MISSING_RECORDS_ARRAY(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' key not found in additionalFields. Specify a 'records' key in addtionalFields.") case MISSING_RECORDS_IN_ADDITIONAL_FIELDS(code: Int = 404, message: String = "\(LangAndVersion) Validation error.'records' object cannot be empty within additionalFields. Specify a non-empty value instead.") case EMPTY_RECORDS_OBJECT(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") case MISSING_RECORDS_IN_GETBYID(code: Int = 404, message: String = "\(LangAndVersion) Validation error. 'records' key cannot be empty. Provide a non-empty value instead.") @@ -69,18 +68,19 @@ internal enum ErrorCodes: CustomStringConvertible { case UPSERT_OPTION_CANNOT_BE_EMPTY(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'upsert' key cannot be an empty array in insert options. Make sure to add atleast one table column object in upsert array.") case MISSING_COLUMN_NAME_IN_USERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Missing 'column' key in upsert array at index . Provide a valid 'column' key.", value: String) case COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) + case UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. 'uniqueColumns' cannot be empty in upsert option at index . Specify at least one column.", value: String) case TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(code: Int = 400, message: String = "\(LangAndVersion) Validation error. Invalid 'table' key in upsert array at index . Specify a value of type string instead.", value: String) case MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(code: Int = 400, message: String = "\(LangAndVersion) Mount failed. Invalid layout array values. Make sure all values in the layout array are positive numbers.") var code: Int { switch self { // No Formatting required // swiftlint:disable:next line_length - case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_ARRAY(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): + case .EMPTY_TABLE_NAME(let code, _), .EMPTY_VAULT_ID(let code, _), .EMPTY_VAULT_URL(let code, _),.RECORDS_KEY_ERROR( let code, _),.EMPTY_TOKEN_ID(let code, _), .ID_KEY_ERROR(let code, _), .INVALID_RECORDS_TYPE(let code, _), .EMPTY_COLUMN_NAME(let code, _), .INVALID_BEARER_TOKEN_FORMAT(let code, _), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS(let code, _), .EMPTY_RECORDS_OBJECT(let code, _), .MISSING_RECORDS_IN_GETBYID(let code, _),.INVALID_URL(let code, _), .INVALID_IDS_TYPE(let code, _),.REDACTION_WITH_TOKEN_NOT_SUPPORTED(let code, _), .TOKENS_GET_COLUMN_NOT_SUPPPORTED(let code, _), .MISSING_COLUMN_NAME(let code,_), .UPSERT_OPTION_CANNOT_BE_EMPTY(let code, _),.INVALID_REDACTION_TYPE(let code, _),.EMPTY_COLUMN_NAME_IN_COLLECT(let code, _), .EMPTY_TABLE_NAME_IN_COLLECT(let code, _), .REGEX_MATCH_FAILED(let code, _),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED(let code, _),.MISSING_IDS_OR_COLUMN_VALUES_IN_GET(let code, _), .MISSING_RECORD_COLUMN_VALUE(let code, _), .INVALID_COLUMN_VALUES_IN_GET(let code, _), .EMPTY_COMPOSABLE_LAYOUT_ARRAY(let code, _), .MISSING_COMPOSABLE_LAYOUT_KEY(let code, _), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(let code, _),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: let code, message: _): return code // Single value formatting // swiftlint:disable:next line_length // Multi value formatting - case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _),.MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : + case .EMPTY_IDS(let code, _, _), .EMPTY_ID_VALUE(let code, _, _), .TABLE_KEY_ERROR(let code, _, _), .FIELDS_KEY_ERROR(let code, _, _), .EMPTY_FIELDS_KEY(let code, _, _), .REDACTION_KEY_ERROR(let code, _, _), .MISSING_KEY_IDS(let code, _, _), .INVALID_TABLE_NAME_TYPE(let code, _, _), .INVALID_FIELDS_TYPE(let code, _, _), .INVALID_TOKEN_TYPE(let code, _, _), .MISSING_TABLE_NAME_IN_USERT_OPTION(let code, _, _), .MISSING_COLUMN_NAME_IN_USERT_OPTION(let code, _, _), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(let code, _, _), .APIError(let code, _), .INVALID_COLUMN_NAME(let code, _, _), .UNMOUNTED_COLLECT_ELEMENT(let code, _, _), .UNMOUNTED_REVEAL_ELEMENT(let code, _, _), .ERROR_TRIGGERED(let code, _, _), .DUPLICATE_ADDITIONAL_FIELD_FOUND(let code, _, _), .EMPTY_RECORD_COLUMN_VALUES(code: let code, message: _, value: _), .EMPTY_COLUMN_VALUE(code: let code, message: _, value: _),.DUPLICATE_ELEMENT_FOUND(let code, _, _) : return code } @@ -90,11 +90,11 @@ internal enum ErrorCodes: CustomStringConvertible { switch self { // No Formatting required // swiftlint:disable:next line_length - case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_ARRAY( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): + case .APIError(_, let message), .INVALID_REDACTION_TYPE( _, let message), .EMPTY_COLUMN_NAME_IN_COLLECT( _, let message), .EMPTY_TABLE_NAME_IN_COLLECT( _, let message), .REGEX_MATCH_FAILED( _, let message), .EMPTY_TABLE_NAME( _, let message), .EMPTY_VAULT_ID( _, let message), .EMPTY_VAULT_URL( _, let message), .RECORDS_KEY_ERROR( _, let message),.INVALID_RECORDS_TYPE( _, let message), .EMPTY_COLUMN_NAME( _, let message), .INVALID_BEARER_TOKEN_FORMAT( _, let message), .MISSING_RECORDS_IN_ADDITIONAL_FIELDS( _, let message), .EMPTY_RECORDS_OBJECT( _, let message), .MISSING_RECORDS_IN_GETBYID( _, let message),.INVALID_URL( _, let message),.SKYFLOW_IDS_AND_COLUMN_NAME_BOTH_SPECIFIED( _, let message), .MISSING_IDS_OR_COLUMN_VALUES_IN_GET( _, let message), .MISSING_RECORD_COLUMN_VALUE( _, let message), .UPSERT_OPTION_CANNOT_BE_EMPTY( _, let message),.INVALID_COLUMN_VALUES_IN_GET( _, let message),.MISSING_COMPOSABLE_LAYOUT_KEY(_, let message), .MISMATCH_ELEMENT_COUNT_LAYOUT_SUM(_, let message),.EMPTY_TOKEN_ID( _, let message), .ID_KEY_ERROR( _, let message),.REDACTION_WITH_TOKEN_NOT_SUPPORTED( _, let message), .TOKENS_GET_COLUMN_NOT_SUPPPORTED( _, let message),.MISSING_COLUMN_NAME( _, let message),.EMPTY_COMPOSABLE_LAYOUT_ARRAY(_, let message), .INVALID_IDS_TYPE( _, let message),.MISSING_COMPOSABLE_CONTAINER_OPTIONS(code: _, message: let message): return message // Single value formatting // swiftlint:disable:next line_length - case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): + case .UNMOUNTED_COLLECT_ELEMENT( _, let message, let value), .UNMOUNTED_REVEAL_ELEMENT( _, let message, let value), .EMPTY_IDS( _, let message, let value), .EMPTY_ID_VALUE( _, let message, let value), .TABLE_KEY_ERROR( _, let message, let value), .FIELDS_KEY_ERROR( _, let message, let value), .EMPTY_FIELDS_KEY( _, let message, let value), .REDACTION_KEY_ERROR( _, let message, let value), .MISSING_KEY_IDS( _, let message, let value), .INVALID_TABLE_NAME_TYPE( _, let message, let value), .INVALID_FIELDS_TYPE( _, let message, let value), .INVALID_TOKEN_TYPE( _, let message, let value), .MISSING_TABLE_NAME_IN_USERT_OPTION( _, let message, let value), .MISSING_COLUMN_NAME_IN_USERT_OPTION( _, let message, let value), .COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(_, let message, let value), .TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION( _, let message, let value), .EMPTY_RECORD_COLUMN_VALUES( _, let message, let value), .EMPTY_COLUMN_VALUE( _, let message, let value), .INVALID_COLUMN_NAME( _, let message, let value), .DUPLICATE_ADDITIONAL_FIELD_FOUND( _, let message, let value), .ERROR_TRIGGERED( _, let message, let value): return formatMessage(message, [value]) // Multi value formatting case .DUPLICATE_ELEMENT_FOUND( _, let message, let values): @@ -121,7 +121,7 @@ internal enum ErrorCodes: CustomStringConvertible { } else { result += values[valuesIndex] } - + valuesIndex += 1 } else { result += word @@ -135,11 +135,11 @@ internal enum ErrorCodes: CustomStringConvertible { public class SkyflowError: NSError { var xml: String = "" - + func setXML(xml: String) { self.xml = xml } - + public func getXML() -> String { return self.xml } diff --git a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift new file mode 100644 index 00000000..f6c7bef4 --- /dev/null +++ b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Used for generating request body for FlowDB v2 detokenize api call + +import Foundation + +internal class FlowVaultDetokenizeRequestBody { + internal static func createRequestBody(vaultID: String, records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil) -> [String: Any] { + let tokens = records.map { $0.token } + var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] + if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { + body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } + } + return body + } +} diff --git a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift new file mode 100644 index 00000000..48a21243 --- /dev/null +++ b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Implementation of callback for Reveal api (FlowDB v2) + +import Foundation + +class FlowVaultRevealAPICallback: Callback { + var apiClient: APIClient + var callback: Callback + var connectionUrl: String + var records: [RevealRequestRecord] + var tokenGroupRedactions: [TokenGroupRedaction]? + var contextOptions: ContextOptions + + + internal init(callback: Callback, apiClient: APIClient, connectionUrl: String, + records: [RevealRequestRecord], tokenGroupRedactions: [TokenGroupRedaction]? = nil, contextOptions: ContextOptions) { + self.apiClient = apiClient + self.callback = callback + self.connectionUrl = connectionUrl + self.records = records + self.tokenGroupRedactions = tokenGroupRedactions + self.contextOptions = contextOptions + } + + internal func onSuccess(_ token: Any) { + guard let url = URL(string: connectionUrl) else { + let errorCode = ErrorCodes.INVALID_URL() + self.callRevealOnFailure(callback: self.callback, errorObject: errorCode.getErrorObject(contextOptions: self.contextOptions)) + return + } + + do { + let (request, session) = try getRequestSession(url: url) + let task = session.dataTask(with: request) { data, response, error in + do { + let response = try self.processResponse(data: data, response: response, error: error) + let errors = response["errors"] as? [[String: Any]] ?? [] + if errors.isEmpty { + self.callback.onSuccess(response) + } else { + self.callback.onFailure(response) + } + } catch { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + task.resume() + } catch let error { + self.callRevealOnFailure(callback: self.callback, errorObject: error) + } + } + + internal func onFailure(_ error: Any) { + if error is Error { + callRevealOnFailure(callback: self.callback, errorObject: error as! Error) + } else { + self.callback.onFailure(error) + } + } + + private func callRevealOnFailure(callback: Callback, errorObject: Error) { + let result = ["errors": [["error": errorObject]]] + callback.onFailure(result) + } + + internal func getRequestSession(url: URL) throws -> (URLRequest, URLSession) { + var jsonString = "" + + do { + let deviceDetails = FetchMetrices().getMetrices() + let jsonData = try JSONSerialization.data(withJSONObject: deviceDetails, options: []) + jsonString = String(data: jsonData, encoding: .utf8) ?? "" + } catch { + jsonString = "" + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + + do { + let data = try JSONSerialization.data(withJSONObject: FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records, tokenGroupRedactions: tokenGroupRedactions)) + request.httpBody = data + } + + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") + request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") + + return (request, URLSession(configuration: .default)) + } + + func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { + if error != nil || response == nil { + throw error! + } + + if let httpResponse = response as? HTTPURLResponse { + let range = 400...599 + if range ~= httpResponse.statusCode { + var description = "Detokenize call failed with the following status code " + String(httpResponse.statusCode) + if let safeData = data { + do { + let errorResponse = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] + if let errorDetails = errorResponse["error"] as? [String: Any], + let message = errorDetails["message"] as? String { + description = message + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + } catch { + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8) ?? "Unknown error").getErrorObject(contextOptions: self.contextOptions) + } + } + throw ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) + } + } + + guard let safeData = data else { + return ["records": [], "errors": []] + } + + return try getDetokenizeResponseBody(data: safeData) + } + + func getDetokenizeResponseBody(data: Data) throws -> [String: Any] { + let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + var successRecords: [[String: Any]] = [] + var errorRecords: [[String: Any]] = [] + + let responseRecords = jsonData["response"] as? [[String: Any]] ?? [] + for entry in responseRecords { + if let error = entry["error"] as? String { + var errorEntry: [String: Any] = ["error": error] + if let token = entry["token"] { errorEntry["token"] = token } + if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } + errorRecords.append(errorEntry) + } else { + var successEntry: [String: Any] = [:] + if let token = entry["token"] { successEntry["token"] = token } + if let value = entry["value"] { successEntry["value"] = value } + if let tokenGroupName = entry["tokenGroupName"] { successEntry["tokenGroupName"] = tokenGroupName } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } + if let metadata = entry["metadata"] as? [String: Any] { successEntry["metadata"] = metadata } + successRecords.append(successEntry) + } + } + + return ["records": successRecords, "errors": errorRecords] + } +} diff --git a/Sources/Skyflow/reveal/RevealApiCallback.swift b/Sources/Skyflow/reveal/RevealApiCallback.swift index 424ea0f6..ad1ec260 100644 --- a/Sources/Skyflow/reveal/RevealApiCallback.swift +++ b/Sources/Skyflow/reveal/RevealApiCallback.swift @@ -54,10 +54,10 @@ class RevealAPICallback: Callback { defer { revealRequestGroup.leave() } - + do { let (success, failure) = try self.processResponse(record: record, data: data, response: response, error: error) - + if success != nil { list_success.append(success!) } @@ -89,7 +89,7 @@ class RevealAPICallback: Callback { let result = ["errors": [["error": errorObject]]] callback.onFailure(result) } - + internal func getRequestSession() -> (URLRequest, URLSession){ var jsonString = "" @@ -107,29 +107,29 @@ class RevealAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(("Bearer " + self.apiClient.token), forHTTPHeaderField: "Authorization") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - + return (request, URLSession(configuration: .default)) } - + internal func getRevealRequestBody(record: RevealRequestRecord) throws -> Data { let bodyObject: [String: Any] = [ "detokenizationParameters": [ [ "token": record.token, - "redaction": record.redaction + "redaction": "PLAIN_TEXT" ] ] ] return try JSONSerialization.data(withJSONObject: bodyObject) } - + internal func processResponse(record: RevealRequestRecord, data: Data?, response: URLResponse?, error: Error?) throws -> (RevealSuccessRecord?, RevealErrorRecord?){ - + if error != nil || response == nil { throw error! } - + if let httpResponse = response as? HTTPURLResponse { let range = 400...599 if range ~= httpResponse.statusCode { @@ -154,13 +154,13 @@ class RevealAPICallback: Callback { let receivedResponseArray: [Any] = (jsonData[keyPath: "records"] as! [Any]) let records: [String: Any] = receivedResponseArray[0] as! [String: Any] let successRecord = RevealSuccessRecord(token_id: records["token"] as! String, value: records["value"] as! String) - + return (successRecord, nil) } - + return (nil, nil) } - + func handleCallbacks(success: [RevealSuccessRecord], failure: [RevealErrorRecord], isSuccess: Bool, errorObject: Error!) { var records: [Any] = [] for record in success { @@ -183,7 +183,7 @@ class RevealAPICallback: Callback { if errors.count != 0 { modifiedResponse["errors"] = errors } - + if isSuccess { if errors.isEmpty { self.callback.onSuccess(modifiedResponse) diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index 57f97bc9..aa426ff3 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -32,7 +32,7 @@ public extension Container { let errorCode = ErrorCodes.EMPTY_VAULT_ID() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - if self.skyflow.vaultURL == "/v1/vaults/" { + if self.skyflow.vaultURL == "/v2/vaults/" { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } @@ -61,8 +61,8 @@ public extension Container { if let tokens = records["records"] as? [[String: Any]] { var list: [RevealRequestRecord] = [] for token in tokens { - if let redaction = token["redaction"] as? RedactionType, let id = token["token"] as? String { - list.append(RevealRequestRecord(token: id, redaction: redaction.rawValue)) + if let id = token["token"] as? String { + list.append(RevealRequestRecord(token: id)) } } let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, @@ -72,7 +72,7 @@ public extension Container { onFailureHandler: { } ) - self.skyflow.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + self.skyflow.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } } } diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/reveal/RevealElementInput.swift index d0da3195..3bf4d7f3 100644 --- a/Sources/Skyflow/reveal/RevealElementInput.swift +++ b/Sources/Skyflow/reveal/RevealElementInput.swift @@ -12,26 +12,14 @@ public struct RevealElementInput { internal var labelStyles: Styles? internal var errorTextStyles: Styles? internal var label: String - internal var redaction: RedactionType internal var altText: String - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, redaction: RedactionType = .PLAIN_TEXT, altText: String = "") { - self.token = token - self.inputStyles = inputStyles - self.labelStyles = labelStyles - self.errorTextStyles = errorTextStyles - self.label = label - self.redaction = redaction - self.altText = altText - } - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { self.token = token self.inputStyles = inputStyles self.labelStyles = labelStyles self.errorTextStyles = errorTextStyles self.label = label - self.redaction = .PLAIN_TEXT self.altText = altText } } diff --git a/Sources/Skyflow/reveal/RevealOptions.swift b/Sources/Skyflow/reveal/RevealOptions.swift index 0857e940..1f63a9e4 100644 --- a/Sources/Skyflow/reveal/RevealOptions.swift +++ b/Sources/Skyflow/reveal/RevealOptions.swift @@ -4,7 +4,7 @@ // // File.swift -// +// // // Created by Akhil Anil Mangala on 11/08/21. // @@ -12,5 +12,9 @@ import Foundation public struct RevealOptions { - public init() {} + public var tokenGroupRedactions: [TokenGroupRedaction]? + + public init(tokenGroupRedactions: [TokenGroupRedaction]? = nil) { + self.tokenGroupRedactions = tokenGroupRedactions + } } diff --git a/Sources/Skyflow/reveal/RevealRequestBody.swift b/Sources/Skyflow/reveal/RevealRequestBody.swift index 72d6d919..8b0230e1 100644 --- a/Sources/Skyflow/reveal/RevealRequestBody.swift +++ b/Sources/Skyflow/reveal/RevealRequestBody.swift @@ -12,7 +12,6 @@ internal class RevealRequestBody { for element in elements { var entry: [String: Any] = [:] entry["token"] = element.revealInput.token - entry["redaction"] = element.revealInput.redaction payload.append(entry) } diff --git a/Sources/Skyflow/reveal/RevealRequestRecord.swift b/Sources/Skyflow/reveal/RevealRequestRecord.swift index 6b1b0b01..2652b247 100644 --- a/Sources/Skyflow/reveal/RevealRequestRecord.swift +++ b/Sources/Skyflow/reveal/RevealRequestRecord.swift @@ -8,9 +8,7 @@ import Foundation struct RevealRequestRecord { var token: String - var redaction: String - init(token: String, redaction: String) { + init(token: String) { self.token = token - self.redaction = redaction } } diff --git a/Sources/Skyflow/reveal/TokenGroupRedaction.swift b/Sources/Skyflow/reveal/TokenGroupRedaction.swift new file mode 100644 index 00000000..c5b3cd51 --- /dev/null +++ b/Sources/Skyflow/reveal/TokenGroupRedaction.swift @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Object that describes a request-level, per-token-group redaction for detokenize + +import Foundation + +public struct TokenGroupRedaction { + public let tokenGroupName: String + public let redaction: String + + public init(tokenGroupName: String, redaction: String) { + self.tokenGroupName = tokenGroupName + self.redaction = redaction + } +} diff --git a/Sources/Skyflow/skyflow_iOS.swift b/Sources/Skyflow/skyflow_iOS.swift deleted file mode 100644 index 226327f6..00000000 --- a/Sources/Skyflow/skyflow_iOS.swift +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -struct skyflow_iOS { - var text = "Hello, World!" -} diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index 78e42f2c..d36871d0 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -579,7 +579,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectSuccess = XCTestExpectation(description: "Should succeed") let myCallback = DemoAPICallback(expectation: expectSuccess) - let records = CollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) + let records = FlowVaultCollectRequestBody.createRequestBody(elements: elements, callback: myCallback, contextOptions: ContextOptions()) let recordElement = (records?["records"] as? [[String: Any]])?[0] let fields = recordElement?["fields"] as? [String: Any] @@ -732,7 +732,7 @@ final class skyflow_iOS_collectTests: XCTestCase { } func testCollectNoVaultURL() { - skyflow.vaultURL = "/v1/vaults/" + skyflow.vaultURL = "/v2/vaults/" let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let expectation = XCTestExpectation() @@ -862,32 +862,6 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - - func testCollectEmptyTableNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() @@ -899,21 +873,21 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectEmptyColumnNameUpsertOption() { + func testCollectEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } func testCollectEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) @@ -923,53 +897,6 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - - - func testInsertEmptyColumnNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - - func testInsertEmptyTableNameForUpsertOption() { - let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["column": "person"]] - let expectation = XCTestExpectation() - let records = [ - "records" : [[ - "table": "card1", - "fields": [ - "person" : "abcfgdyt", - "cvv" : "567" - ] - ]] - ] - let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) - self.skyflow?.insert(records: records, options: insertOptions, callback: callback) - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) - } - - func testInsertEmptyUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() @@ -990,9 +917,9 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testInsertEmptyColumnNameUpsertOption() { + func testInsertEmptyUniqueColumnsUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "card1", "column": ""]] + let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1008,12 +935,12 @@ final class skyflow_iOS_collectTests: XCTestCase { self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UNIQUE_COLUMNS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } func testInsertEmptyTableNameUpsertOption() { let container = skyflow.container(type: ContainerType.COLLECT) - let upsertOptions = [["table": "", "column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -1031,7 +958,7 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - + func testUnmount() { let container = skyflow.container(type: ContainerType.COLLECT) let date = container?.create(input: CollectElementInput(type: .EXPIRATION_DATE), options: CollectElementOptions(format: "test")) diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index a94cd297..be69c6a3 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -455,7 +455,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { } func testInsertEmptyTableNameForUpsertOption() { _ = skyflow.container(type: ContainerType.COMPOSABLE) - let upsertOptions = [["column": "person"]] + let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let records = [ "records" : [[ @@ -471,7 +471,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) - XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } func testCollectBadTableKeyAddionalFields() { let additionalFields = ["records": [["table": []]]] diff --git a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift index b6e84385..21a86795 100644 --- a/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift +++ b/Tests/skyflow-iOS-elementTests/skyflow_iOS_elementTests.swift @@ -26,7 +26,7 @@ class skyflow_iOS_elementTests: XCTestCase { textField = TextField(input: collectInput, options: collectOptions, contextOptions: ContextOptions(), elements: []) - let revealElementInput = RevealElementInput(token: "token", label: "RevealElement", redaction: .DEFAULT) + let revealElementInput = RevealElementInput(token: "token", label: "RevealElement") label = Label(input: revealElementInput, options: RevealElementOptions()) } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 3df67f16..4c5902fe 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -267,7 +267,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { window.addSubview(cvv!) let expectation = XCTestExpectation(description: "Container insert call - Duplicate Elements") let callback = DemoAPICallback(expectation: expectation) - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -298,7 +298,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { "name": "John Doe" ]] ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -334,7 +334,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { "duplicate": "123", ]] ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -368,7 +368,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { "duplicate": "123", ]] ]] - CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) + FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse @@ -389,55 +389,31 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { cvv?.textField.secureText = "211" window.addSubview(cvv!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) XCTAssertNotNil(requestBody?["records"]) XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ ["table": "persons", "fields": ["card_number": "", "cvv": ""]] ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) } - func testCreateRequestBodyWithOnlyUpdates() { + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { let additionalFields: [String: Any] = [ "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] + ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"] ] ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2"], "skyflowID": "id1"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) } - func testCreateRequestBodyWithOnlyUpdates2() { - let window = UIWindow() - let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV) - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Insert only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], callback: callback, contextOptions: ContextOptions()) - XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ - ["table": "persons", "fields": ["card_number": "", "cvv": ""]] - ]) - XCTAssertEqual(requestBody?["update"] as? [String: String], [:]) - } - - func testCreateRequestBodyWithOnlyUpdates3() { + func testCreateRequestBodyWithSkyflowIDOnElement() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) @@ -445,56 +421,15 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"], - ["table": "table1", "fields": ["column2": "value2"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update only")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) - XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["update"]) - XCTAssertEqual(requestBody?["update"] as? NSDictionary, ["id1": ["table": "table1", "fields": ["column1": "value1", "column2": "value2", "card_number": ""], "skyflowID": "id1"], "id2": ["table": "persons", "fields": ["cvv": ""], "skyflowID": "id2"]]) - XCTAssertEqual(requestBody?["records"] as? [[String: String]], []) - } - func testCreateRequestBodyWithMixedInsertsAndUpdates() { - let window = UIWindow() - let container = skyflow.container(type: ContainerType.COLLECT, options: nil) - let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) - let cardNumber = container?.create(input: collectInput1, options: options) - cardNumber?.textField.secureText = "4111 1111 1111 1111" - window.addSubview(cardNumber!) - - let collectInput2 = CollectElementInput(table: "persons", column: "cvv", placeholder: "cvv", type: .CVV, skyflowID: "id2") - let cvv = container?.create(input: collectInput2, options: options) - cvv?.textField.secureText = "211" - window.addSubview(cvv!) - - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"] - ] - ] - let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed inserts and updates")) - let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) - XCTAssertNotNil(requestBody?["records"]) - let records = requestBody?["records"] as? [[String: Any]] - XCTAssertEqual(records?.count, 1) - XCTAssertEqual(records?[0] as! NSDictionary as NSDictionary, ["table": "persons", "fields": ["card_number": ""]]) - XCTAssertNotNil(requestBody?["update"]) - let update = requestBody?["update"] as? [String: Any] - let updateRecords = update?["id1"] as! [String: Any] - XCTAssertEqual(updateRecords as NSDictionary, ["table": "table1", "fields": ["column3": "value3"], "skyflowID": "id1"]) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift index c5dfc98d..faf14185 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift @@ -69,14 +69,6 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { } - func testGetByIDRecord() { - let record = GetByIdRecord(ids: ["id1", "id2"], table: "table", redaction: "DEFAULT") - - XCTAssertEqual(record.ids, ["id1", "id2"]) - XCTAssertEqual("table", record.table) - XCTAssertEqual(record.redaction, "DEFAULT") - } - func testValidationSet() { let validationSet = ValidationSet( rules: [SkyflowValidateCardNumber( diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index 038716e6..cce57190 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -48,26 +48,8 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } } - func getByIDFromClientWithExpectation(description: String = "should get records", records: [String: Any]) -> String { - let expectRecords = XCTestExpectation(description: description) - let callback = DemoAPICallback(expectation: expectRecords) - skyflow.getById(records: records, callback: callback) - - wait(for: [expectRecords], timeout: 10.0) - if callback.receivedResponse.isEmpty { - if callback.data["errors"] != nil { - return (callback.data["errors"] as! [NSError])[0].localizedDescription - } else { - return "ok" - } - } else { - return callback.receivedResponse - } - } - - func testDetokenizeNoRecords() { - let records = ["typo": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] + let records = ["typo": [["token": revealTestId]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.RECORDS_KEY_ERROR().description) } @@ -77,7 +59,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) } - + func testDetokenizeEmptyRecords() { let records = ["records": []] let result = getDataFromClientWithExpectation(records: records) @@ -85,21 +67,16 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { } func testDetokenizeNoTokens() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] + let records = ["records": [["foo": "bar"]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.ID_KEY_ERROR().description) } func testDetokenizeBadTokens() { - let records = ["records": [["token": [], "redaction": RedactionType.DEFAULT]]] + let records = ["records": [["token": []]]] let result = getDataFromClientWithExpectation(records: records) XCTAssertEqual(result, ErrorCodes.INVALID_TOKEN_TYPE(value: "0").description) } - func testDetokenizeInvalidRedaction() { - let records = ["records": [["token": "123233232", "redaction": "123"]]] - let result = getDataFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } func testContainerRevealWithUnmountedElements() { @@ -118,20 +95,6 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { XCTAssertEqual(result, ErrorCodes.UNMOUNTED_REVEAL_ELEMENT(value: revealTestId).description) } - func testContainerRevealWithNoRedaction() { // when redaction is not specify plaintext will be applied - let window = UIWindow() - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let styles = Styles(base: bstyle) - - let revealElementInput = RevealElementInput(token: "232h3j23h2jh",inputStyles: styles, label: "RevealElement") - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - window.addSubview(revealElement!) - XCTAssertEqual(revealElement!.revealInput.redaction, .PLAIN_TEXT) - } - func testContainerRevealWithEmptyToken() { let window = UIWindow() let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) @@ -139,7 +102,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) let styles = Styles(base: bstyle) - let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) + let revealElementInput = RevealElementInput(inputStyles: styles, label: "RevealElement") let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) window.addSubview(revealElement!) @@ -152,51 +115,4 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { XCTAssertEqual(result, ErrorCodes.EMPTY_TOKEN_ID().description) } - func testGetByIdNoRecords() { - let records = ["ok": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.EMPTY_RECORDS_OBJECT().description) - } - - func testGetByIdInvalidRecords() { - let records = ["records": 12] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_RECORDS_TYPE().description) - } - - func testGetByIdNoIds() { - let records = ["records": [["redaction": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.MISSING_KEY_IDS(value: "0").description) - } - - func testGetByIdInvalidIds() { - let records = ["records": [["ids": RedactionType.DEFAULT]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_IDS_TYPE().description) - } - - func testGetByIdNoTable() { - let records = ["records": [["ids": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.TABLE_KEY_ERROR(value: "0").description) - } - - func testGetByIdInvalidTable() { - let records = ["records": [["ids": ["abc"], "table": ["abc"]]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").description) - } - - func testGetByIdNoRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.REDACTION_KEY_ERROR(value: "0").description) - } - - func testGetByIdInvalidRedaction() { - let records = ["records": [["ids": ["abc"], "table": "table", "redaction": "DEFAULT"]]] - let result = getByIDFromClientWithExpectation(records: records) - XCTAssertEqual(result, ErrorCodes.INVALID_REDACTION_TYPE().description) - } } diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 101264b8..2905454a 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -38,15 +38,6 @@ class skyflow_iOS_revealTests: XCTestCase { let istyle = Style(textColor: .red) let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement", redaction: .DEFAULT) - - return revealElementInput - } - func getRevealElementInputNoRedaction() -> RevealElementInput { - let bstyle = Style(borderColor: UIColor.blue, cornerRadius: 20, padding: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 5), borderWidth: 2, textColor: UIColor.blue) - let istyle = Style(textColor: .red) - let styles = Styles(base: bstyle, invalid: istyle) - let revealElementInput = RevealElementInput(token: revealTestId, inputStyles: styles, label: "RevealElement") return revealElementInput @@ -65,7 +56,6 @@ class skyflow_iOS_revealTests: XCTestCase { let revealElementInput = getRevealElementInput() XCTAssertEqual(revealElementInput.token, revealTestId) - XCTAssertEqual(revealElementInput.redaction, .DEFAULT) XCTAssertEqual(revealElementInput.label, "RevealElement") } @@ -104,22 +94,11 @@ class skyflow_iOS_revealTests: XCTestCase { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) let revealElementInput = getRevealElementInput() let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - - let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.DEFAULT]]] - - XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) - } - func testCreateRevealRequestBodyWithNoRedaction() { - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - let revealElementInput = getRevealElementInputNoRedaction() - let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) - + let requestBody = RevealRequestBody.createRequestBody(elements: [revealElement!]) as! [String: [[String: Any]]] - - let result: [String: [[String: Any]]] = ["records": [["token": revealTestId, "redaction": RedactionType.PLAIN_TEXT]]] - + + let result: [String: [[String: Any]]] = ["records": [["token": revealTestId]]] + XCTAssertTrue(compareDictionaries(dict1: result, dict2: requestBody)) } diff --git a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift index a0189fdb..33f86654 100644 --- a/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift +++ b/Tests/skyflow-iOS-scenarioTests/ClientScenario.swift @@ -47,7 +47,7 @@ class ClientScenario { func getById(ids: [String: Any], callback: Callback) { Client(self.config).getById(records: ids, callback: callback) } - + private func detokenizeRequestBody(_ tokens: [String]) -> [String: [[String: String]]]{ var records = [] as [[String: String]] diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 40bbe132..1dadd6b2 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -7,130 +7,153 @@ import XCTest final class skyflow_iOS_collectUtilTests: XCTestCase { - var collectCallback: CollectAPICallback! = nil + var collectCallback: FlowVaultCollectAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = CollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(tokens: false, additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - - let result = callback.data["errors"] as! [[String: Any]] - let errorObject = result[0]["error"] as! [String: Any] + + let errorObject = callback.data["error"] as! [String: Any] let msg = errorObject["message"] as! String XCTAssert(msg.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + let errors = result["errors"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertNil(records[0]["tokens"]) + XCTAssertTrue(errors.isEmpty) } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["tokens"] as! [String: String], ["field": "value"]) } catch { XCTFail(error.localizedDescription) } } - + + func testGetCollectResponseWithDataAndHashedDataAndHttpCode() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) + } catch { + XCTFail(error.localizedDescription) + } + } + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) - as! [String: [String: String]] + as! [String: [String: String]] } catch { } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - + // XCTFail("Not throwing on Api Error") do { var res = try self.collectCallback.processResponse(data: data, response: response, error: nil) @@ -143,7 +166,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) @@ -151,11 +174,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let container = client.container(type: ContainerType.COLLECT) let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - + container?.collect(callback: callback) - + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -167,122 +190,32 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { XCTAssertTrue(deviceDetails["sdk_name_version"] as! String == "") } } - func testUpdateNewFlow() { - let expectation = XCTestExpectation(description: "Update new flow should succeed and merge response") - let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) - // Simulate a successful update response - let responseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) - let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - do { - let processed = try collectCallback.processUpdateResponse(data: responseData, response: urlResponse, error: nil, table: "table") - let records = processed["records"] as! [[String: Any]] - print("records after processing update response", records) - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as? String, "table") - let fields = records[0]["fields"] as? [String: String] - XCTAssertEqual((fields?["skyflow_id"] ?? "") as String, "id1") - XCTAssertEqual(fields?["field"] as? String, "newValue") - } catch { - XCTFail("Update flow failed: \(error)") - } - } - func testPartialInsertAndUpdateScenario() { - let expectation = XCTestExpectation(description: "Partial insert and update scenario should succeed") - let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] - - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) - - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(insertRecords.count, 1) - let ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "SID") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") - } catch { - XCTFail("Partial insert and update scenario failed: \(error)") - } - } func testOnlyInsertSuccess() { let expectation = XCTestExpectation(description: "Only insert records should succeed") let callback = DemoAPICallback(expectation: expectation) @@ -292,287 +225,272 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(tokens: true, additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) let insertRecords = processedInsert["records"] as! [[String: Any]] + let errors = processedInsert["errors"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let tokens = insertRecords[0]["tokens"] as? [String: String] + XCTAssertEqual(tokens?["field"], "value") + XCTAssertTrue(errors.isEmpty) } catch { XCTFail("Insert scenario failed: \(error)") } } - func testOnlyUpdateSuccess() { - let expectation = XCTestExpectation(description: "Only update records should succeed") + func testInsertPartialFailure() { + let expectation = XCTestExpectation(description: "Partial insert failure should populate errors array") let callback = DemoAPICallback(expectation: expectation) - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(tokens: true, additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]], + ["tableName": "table", "error": "insert failed", "httpCode": 400] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"] as? String, "insert failed") + XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Update scenario failed: \(error)") + XCTFail("Partial insert failure scenario failed: \(error)") } } - func testInsertAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert and update records should succeed") + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] + let insertRecords: [[String: Any]] = [ + ["table": "table", "fields": ["field": "value"]], + ["table": "table", "fields": ["field": "value2"]] ] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(tokens: true, additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response -// let insertResponseDict: [String: Any] = [ -// "records": [["skyflow_id": "inserted_id"]] -// ] - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - - let insertRecords = processedInsert["records"] as! [[String: Any]] - let updateRecords = processedUpdate["records"] as! [[String: Any]] + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] - XCTAssertEqual(insertRecords.count, 1) - var ifields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(ifields?["skyflow_id"] as? String, "inserted_id") - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + XCTAssertTrue(records.isEmpty) + XCTAssertEqual(errors.count, 2) + XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert and update scenario failed: \(error)") + XCTFail("Full failure scenario should not throw: \(error)") } } - func testInsertSuccessAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert success and update failure") + func testInsertPartialFailureWith207OuterStatus() { + let expectation = XCTestExpectation(description: "Partial failure with 207 outer status should parse per-record errors") let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] + let insertRecords: [[String: Any]] = [ + ["table": "table2", "fields": ["address": "dedede", "gender": "sagar"]], + ["table": "table", "fields": ["field": "value"]] ] - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( + let collectCallback = FlowVaultCollectAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + records: ["records": insertRecords], + options: FlowVaultICOptions(tokens: true, additionalFields: nil), contextOptions: ContextOptions() ) - // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "inserted_id"]]], ["fields": ["field": "value"]]]] - let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - // Simulate a failed update response - let updateError = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Update failed"]) + let responseDict: [String: Any] = ["records": [ + ["skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", "tokens": ["address": [["token": "dedwim", "tokenGroupName": "deterministic_string"]]], + "data": ["address": "dedede", "gender": "sagar"], "hashedData": [:], "error": NSNull(), "httpCode": 200, "tableName": "table2"], + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) do { - let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) - let insertRecords = processedInsert["records"] as! [[String: Any]] - - XCTAssertEqual(insertRecords.count, 1) - var fields = insertRecords[0]["fields"] as? [String: Any] - XCTAssertEqual(fields?["skyflow_id"] as? String, "inserted_id") + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] - let insertResponse = try collectCallback.processUpdateResponse(data: nil, response: nil, error: updateError, table: "table") as! [String: [String: String]] - XCTAssertEqual(insertResponse, ["error": ["message": "Update failed"]]) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert success and update failure scenario failed: \(error)") + XCTFail("Partial (207) scenario should not throw: \(error)") } } - func testInsertFailureAndUpdateSuccess() { - let expectation = XCTestExpectation(description: "Insert failure and update success") - let callback = DemoAPICallback(expectation: expectation) - - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] - - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] - - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) - - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) - - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + func testUpdateSuccessResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "f30c8ccf-7e86-46b4-be74-0b2db44e4b87", + "tokens": { + "email": [{"token": "a@ehmw.aqk", "tokenGroupName": "nondeterministic_string"}], + "name": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}], + "passport": [{"token": "21284054160", "tokenGroupName": "deterministic_string"}] + }, + "data": {"email": "b@demo.com", "name": "sagar", "passport": "21212121212"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table1" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions(tokens: true) do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) - - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") - let updateRecords = processedUpdate["records"] as! [[String: Any]] - - XCTAssertEqual(updateRecords.count, 1) - XCTAssertEqual(updateRecords[0]["table"] as? String, "table") - let fields = updateRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"], "id1") - XCTAssertEqual(fields?["field"], "newValue") + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] + + XCTAssertTrue(errors.isEmpty) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflowID"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") + let data = records[0]["data"] as! [String: Any] + XCTAssertEqual(data["name"] as? String, "sagar") + let tokens = records[0]["tokens"] as! [String: Any] + let nameTokens = tokens["name"] as! [[String: Any]] + XCTAssertEqual(nameTokens[0]["token"] as? String, "sagwm") } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") + XCTFail("Update success response should not throw: \(error)") } } - func testInsertFailureAndUpdateFailure() { - let expectation = XCTestExpectation(description: "Insert failure and update failure") - let callback = DemoAPICallback(expectation: expectation) - let insertRecord: [String: Any] = [ - "table": "table", - "fields": ["field": "value"] - ] - - let updateRecord: [String: Any] = [ - "table": "table", - "skyflowID": "id1", - "fields": ["field": "newValue"] - ] + func testUpdateFullFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) - let collectCallback = CollectAPICallback( - callback: callback, - apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), - records: ["records": [insertRecord], "update": ["id1": updateRecord]], - options: ICOptions(tokens: true, additionalFields: nil), - contextOptions: ContextOptions() - ) + do { + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] - // Simulate a failed insert response - let insertErrorResponseDict: [String: Any] = [ - "error": ["message": "Insert failed"] - ] - let insertErrorResponseData = try! JSONSerialization.data(withJSONObject: insertErrorResponseDict, options: .fragmentsAllowed) - let insertErrorUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + XCTAssertTrue(records.isEmpty) + XCTAssertEqual(errors.count, 2) + XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + } catch { + XCTFail("Update full failure response should not throw: \(error)") + } + } - // Simulate a successful update response - let updateResponseDict: [String: Any] = [ - "skyflow_id": "id1", - "tokens": ["field": "newValue"] - ] - let updateErrorResponseDict: [String: Any] = [ - "error": ["message": "Update failed"] - ] - let updateResponseData = try! JSONSerialization.data(withJSONObject: updateErrorResponseDict, options: .fragmentsAllowed) - let updateUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/table/id1")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + func testUpdatePartialFailureResponseLiteral() { + let json = """ + { + "records": [ + { + "skyflowID": "b187b5b6-28b4-4881-93fa-4ef10e30b20e", + "tokens": { + "address": [{"token": "dedwim", "tokenGroupName": "deterministic_string"}], + "gender": [{"token": "sagwm", "tokenGroupName": "deterministic_string"}] + }, + "data": {"address": "dedede", "gender": "sagar"}, + "hashedData": {}, + "error": null, + "httpCode": 200, + "tableName": "table2" + }, + { + "skyflowID": null, "tokens": null, "data": null, "hashedData": null, + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": "" + } + ] + } + """ + let responseData = json.data(using: .utf8)! + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) + self.collectCallback.options = FlowVaultICOptions(tokens: true) do { - let insertProccessed = try collectCallback.processResponse(data: insertErrorResponseData, response: insertErrorUrlResponse, error: nil) as! [String: [String: Any]] - XCTAssertEqual(insertProccessed["error"]?["message"] as? String, "Insert failed") - XCTAssertEqual(insertProccessed["error"]?["code"] as? Int, 404) - - let processedUpdate = try collectCallback.processUpdateResponse(data: updateResponseData, response: updateUrlResponse, error: nil, table: "table") as! [String: [String: Any]] - - XCTAssertEqual(processedUpdate["error"]?["message"] as? String, "Update failed") - XCTAssertEqual(processedUpdate["error"]?["code"] as? Int, 404) + let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { - XCTFail("Insert failure and update success scenario failed: \(error)") + XCTFail("Update partial response should not throw: \(error)") } } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index e3becb93..ba71c627 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -7,137 +7,160 @@ import XCTest final class skyflow_iOS_insertUtilTests: XCTestCase { - var collectCallback: InsertAPICallback! = nil + var collectCallback: FlowVaultInsertAPICallback! = nil var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] - + override func setUp() { - self.collectCallback = InsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), + self.collectCallback = FlowVaultInsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: ICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(tokens: false, additionalFields: nil), contextOptions: ContextOptions()) } - + func testBuildFieldsDict() { let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] let result = self.collectCallback.buildFieldsDict(dict: dict) XCTAssertEqual(dict["key"] as! String, result["key"] as! String) XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) } - + func testOnSuccessInvalidUrl() { let expectation = XCTestExpectation(description: "Invalid URL should trigger failure") let callback = DemoAPICallback(expectation: expectation) self.collectCallback.apiClient.vaultURL = "Invalid url" self.collectCallback.callback = callback - + self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - + let result = callback.receivedResponse XCTAssert(result.contains("unsupported URL")) } - + func testGetRequestSession() { let url = URL(string: "https://example.org")! do { let (request, session) = try self.collectCallback.getRequestSession(url: url) - + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") // From DemoTokenProvider() + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] let records = body["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["fields"] as! [String: String], ["field": "value"]) - XCTAssertEqual(records[0]["method"] as! String, "POST") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) XCTAssertEqual(records[0]["tableName"] as! String, "table") - + } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) - - XCTAssertEqual(result as! [String: [[String: String]]], ["records": [ - [ - "table": "table", - "skyflow_id": "SID" - ] - ]]) + let records = result["records"] as! [[String: Any]] + let errors = result["errors"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertTrue(errors.isEmpty) } catch { XCTFail(error.localizedDescription) } } - + func testGetCollectResponseWithTokens() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - self.collectCallback.options = ICOptions() - + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] as [String: Any] + self.collectCallback.options = FlowVaultICOptions() + + do { + let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["tokens"] as! [String: String], ["field": "value"]) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseWithDataAndHashedDataAndHttpCode() { + let response: [String: Any] = ["records": [[ + "skyflowID": "SID", + "tableName": "table", + "httpCode": 200, + "data": ["field": "value"], + "hashedData": ["field": "hashed-value"] + ]]] + self.collectCallback.options = FlowVaultICOptions() + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - - let expected = ["records": [["table": "table", "fields": ["field": "value", "skyflow_id": "SID"]]]] - - XCTAssertEqual(records.count, expected["records"]!.count) - XCTAssertEqual(records[0]["table"] as! String, expected["records"]![0]["table"] as! String) - XCTAssertEqual(records[0]["fields"] as! [String: String], expected["records"]![0]["fields"] as! [String: String]) - - + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) + XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponse() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) as! [String: [[String: String]]] - XCTAssertEqual(processedData, ["records": [["skyflow_id": "SID", "table": "table"]]]) - + + let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let records = processedData["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + } catch { XCTFail(error.localizedDescription) } } - + func testProcessResponseError() { - let response = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] - + let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) as! [String: [[String: String]]] + + _ = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) XCTFail("Should have thrown error") - + } catch { } } - + func testProcessResponseFailure() { let response = ["error": ["message": "Internal Server Error"]] do { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - - try self.collectCallback.processResponse(data: data, response: response, error: nil) + + _ = try self.collectCallback.processResponse(data: data, response: response, error: nil) XCTFail("Not throwing on Api Error") - + } catch { XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testCollectInvalidBearerToken() { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) @@ -145,11 +168,11 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let container = client.container(type: ContainerType.COLLECT) let input = CollectElementInput(table: "table", column: "column", type: .EXPIRATION_YEAR) let element = container?.create(input: input) - + UIWindow().addSubview(element!) - + container?.collect(callback: callback) - + wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } @@ -161,25 +184,25 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { XCTAssertEqual(device.systemName + "@" + device.systemVersion, deviceInfo["sdk_client_os_details"] as! String) } func testDeviceDetails() { - + let deviceDetails = FetchMetrices().getDeviceDetails() - + XCTAssertNotNil(deviceDetails["device"]) XCTAssertNotNil(deviceDetails["os_details"]) XCTAssertNotNil(deviceDetails["sdk_name_version"]) - + if let device = deviceDetails["device"] as? String { XCTAssertFalse(device.isEmpty) } else { XCTAssertTrue(deviceDetails["device"] as! String == "") } - + if let osDetails = deviceDetails["os_details"] as? String { XCTAssertFalse(osDetails.isEmpty) } else { XCTAssertTrue(deviceDetails["os_details"] as! String == "") } - + if let sdkNameVersion = deviceDetails["sdk_name_version"] as? String { XCTAssertFalse(sdkNameVersion.isEmpty) } else { @@ -196,30 +219,70 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { "fields": ["field": "value"] ] - let collectCallback = InsertAPICallback( + let collectCallback = FlowVaultInsertAPICallback( callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: ICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(tokens: true, additionalFields: nil), contextOptions: ContextOptions() ) // Simulate a successful insert response - let insertResponseDict = ["responses": [["records": [["skyflow_id": "SID"]]], ["fields": ["field": "value"]]]] + let insertResponseDict: [String: Any] = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] let insertResponseData = try! JSONSerialization.data(withJSONObject: insertResponseDict, options: .fragmentsAllowed) - let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/vault")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let insertUrlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) let insertRecords = processedInsert["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - let fields = insertRecords[0]["fields"] as? [String: String] - XCTAssertEqual(fields?["skyflow_id"] as? String, "SID") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") + let tokens = insertRecords[0]["tokens"] as? [String: String] + XCTAssertEqual(tokens?["field"], "value") } catch { XCTFail("Insert scenario failed: \(error)") } } + func testInsertFullFailureWithNon2xxOuterStatus() { + let expectation = XCTestExpectation(description: "Full batch failure with 400 outer status should still parse per-record errors") + let callback = DemoAPICallback(expectation: expectation) + + let insertRecord: [String: Any] = [ + "table": "table", + "fields": ["field": "value"] + ] + + let collectCallback = FlowVaultInsertAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [insertRecord]], + options: FlowVaultICOptions(tokens: true, additionalFields: nil), + contextOptions: ContextOptions() + ) + + let responseDict: [String: Any] = ["records": [ + ["skyflowID": NSNull(), "tokens": NSNull(), "data": NSNull(), "hashedData": NSNull(), + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "httpCode": 400, "tableName": ""] + ]] + let responseData = try! JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/insert")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + + do { + let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + let records = processed["records"] as! [[String: Any]] + let errors = processed["errors"] as! [[String: Any]] + + XCTAssertTrue(records.isEmpty) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + } catch { + XCTFail("Full failure scenario should not throw: \(error)") + } + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index f09f9108..0a9b4df6 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -9,7 +9,7 @@ import XCTest // swiftlint:disable:next type_body_length final class skyflow_iOS_revealUtilTests: XCTestCase { - var revealApiCallback: RevealAPICallback! = nil + var revealApiCallback: FlowVaultRevealAPICallback! = nil var revealValueCallback: RevealValueCallback! = nil var expectation: XCTestExpectation! = nil var callback: DemoAPICallback! = nil @@ -20,7 +20,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { override func setUp() { self.expectation = XCTestExpectation() self.callback = DemoAPICallback(expectation: self.expectation) - self.revealApiCallback = RevealAPICallback(callback: self.callback, + self.revealApiCallback = FlowVaultRevealAPICallback(callback: self.callback, apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), connectionUrl: "", records: [], @@ -40,7 +40,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { func testOnSuccessInvalidUrl() { self.revealApiCallback.connectionUrl = "invalid url" - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) + let record = RevealRequestRecord(token: "token") self.revealApiCallback.records = [record] self.revealApiCallback.onSuccess("token") wait(for: [self.expectation], timeout: 10.0) @@ -48,111 +48,123 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") } - + func testGetRequestSession() { - self.revealApiCallback.connectionUrl = "https://www.example.org" - - let (request, session) = self.revealApiCallback.getRequestSession() - XCTAssertEqual(request.url?.absoluteString, "https://www.example.org/detokenize") - XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json; utf-8") - XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") - XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") - } - - func testRevealRequestBody() { - let record = RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue) - do { - let result = try self.revealApiCallback.getRevealRequestBody(record: record) + let url = URL(string: "https://www.example.org")! - if let jsonObject = try? JSONSerialization.jsonObject(with: result, options: []) as? [String: Any] { - let key = jsonObject.keys - let values = jsonObject.values - XCTAssertTrue(key.contains("detokenizationParameters")) - } else { - print("Failed to convert JSON data into a JSON object.") - } - XCTAssertNotNil(result) + do { + let (request, session) = try self.revealApiCallback.getRequestSession(url: url) + XCTAssertEqual(request.url?.absoluteString, "https://www.example.org") + XCTAssertEqual(request.allHTTPHeaderFields!["Content-Type"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields!["Authorization"], "Bearer ") } catch { XCTFail(error.localizedDescription) } } - + + func testConstructV2DetokenizeRequestBody() { + self.revealApiCallback.records = [RevealRequestRecord(token: "token1"), RevealRequestRecord(token: "token2")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: self.revealApiCallback.records) + + XCTAssertEqual(result["vaultID"] as! String, "vault123") + XCTAssertEqual(result["tokens"] as! [String], ["token1", "token2"]) + XCTAssertNil(result["tokenGroupRedactions"]) + } + + func testConstructV2DetokenizeRequestBodyWithTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED")] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 1) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + } + func testProcessResponseError() { let revealedResponse = ["key": "value"] let serverError = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Internal Server Error"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: nil, error: serverError) + _ = try self.revealApiCallback.processResponse(data: responseData, response: nil, error: serverError) XCTFail("Not throwing on http error") } catch { XCTAssertEqual(error.localizedDescription, serverError.localizedDescription) } } - + func testProcessResponseBadCode() { let revealedResponse = ["error": ["message": "Internal Server Error"]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(success) - XCTAssertNotNil(failure) - XCTAssertEqual(failure?.error.localizedDescription, "Internal Server Error - request-id: RID") + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Not throwing on http error") } catch { - XCTFail(error.localizedDescription) + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") } } - + func testProcessResponseSuccess() { - let revealedResponse = ["records": [["token": "token", "value": "value"]]] + let revealedResponse = ["response": [["token": "token", "value": "value"]]] let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) do { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) - let (success, failure) = try self.revealApiCallback.processResponse(record: RevealRequestRecord(token: "token", redaction: RedactionType.PLAIN_TEXT.rawValue), data: responseData, response: httpResponse, error: nil) - XCTAssertNil(failure) - XCTAssertNotNil(success) - XCTAssertEqual(success?.token_id, "token") - XCTAssertEqual(success?.value, "value") + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + let errors = response["errors"] as! [[String: Any]] + XCTAssertTrue(errors.isEmpty) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "token") + XCTAssertEqual(records[0]["value"] as? String, "value") } catch { XCTFail(error.localizedDescription) } } - - func testHandleCallbacksSuccess() { - let errorObject = NSError(domain: "", code: 200, userInfo: nil) - let success = [RevealSuccessRecord(token_id: "token", value: "value")] - - self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let records = self.callback.data["records"] as! [[String: String]] - - XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["value"], "value") - XCTAssertEqual(records[0]["token"], "token") - } - - func testHandleCallbacksFailure() { - let failure = RevealErrorRecord(id: "token", error: NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"])) - self.revealApiCallback.handleCallbacks(success: [], failure: [failure], isSuccess: true, errorObject: nil) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, failure.error) - XCTAssertEqual(errors[0]["token"] as! String, "token") + + func testProcessResponseWithMetadataAndHttpCode() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token", "value": "value", "httpCode": 200, "metadata": ["tableName": "table", "skyflowID": "SID"]] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) + let metadata = records[0]["metadata"] as! [String: Any] + XCTAssertEqual(metadata["tableName"] as? String, "table") + XCTAssertEqual(metadata["skyflowID"] as? String, "SID") + } catch { + XCTFail(error.localizedDescription) + } } - - func testHandleCallbacksError() { - let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]) - self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: error) - - wait(for: [self.expectation], timeout: 20.0) - let errors = self.callback.data["errors"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as! NSError, error) + + func testProcessResponsePartialFailure() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "token1", "value": "value1"], + ["token": "token2", "error": "Invalid Token", "httpCode": 400] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + let errors = response["errors"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "token1") + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "token2") + XCTAssertEqual(errors[0]["error"] as? String, "Invalid Token") + } catch { + XCTFail(error.localizedDescription) + } } - + func testGetTokensToErrors() { let errors = [["token": "1234"], ["token": "4321"]] diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index 6e872245..f4534f1e 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -61,30 +61,20 @@ final class skyflow_iOS_utilTests: XCTestCase { textField.addAndFormatText(str) XCTAssertEqual(textField.secureText, "4111 1111 1111 1111") } - func testConstructRequestWithTokens() { - let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) - let result = apiClient.constructBatchRequestBody(records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: ICOptions(tokens: true)) - let record = result["records"] as! [[String: Any]] - let id = record[1]["ID"] as! String - let tokenization = record[1]["tokenization"] as! Bool - XCTAssertEqual(id, "$responses.0.records.0.skyflow_id") - XCTAssertTrue(tokenization) + func testConstructV2RequestBody() { + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(tokens: true)) + XCTAssertEqual(result["vaultID"] as! String, "vault123") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["data"] as! [String: String], ["field1": "value1"]) } - - func testConstructUpdateRequestBody() { - let apiClient = APIClient(vaultID: "vault123", vaultURL: "https://example.com", tokenProvider: DemoTokenProvider()) - - let updateRecords: [String: Any] = [ - "update": [ - ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"], - ["table": "users", "fields": ["name": "Jane Doe", "email": "jane.doe@example.com"], "skyflowID": "id124"] - ] - ] - let singleUpdateRecord = ["table": "users", "fields": ["name": "John Doe", "email": "john.doe@example.com"], "skyflowID": "id123"] as [String : Any] - let result = apiClient.constructUpdateRequestBody(records: singleUpdateRecord, options: ICOptions(tokens: false)) - let records = result["record"] as! [String: Any] - let tokenization = result["tokenization"] as! Bool - XCTAssertEqual(records as NSDictionary, ["fields": ["name": "John Doe", "email": "john.doe@example.com"]]) - XCTAssertFalse(tokenization) + + func testConstructV2RequestBodyWithUpsert() { + let upsert = [UpsertOption(table: "table", uniqueColumns: ["field1"], updateType: .REPLACE)] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(tokens: true, upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertEqual(upsertPayload["updateType"] as! String, "REPLACE") } } From fb3c3c169d820ffbd9183c9c339cc001618202c8 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 22 Jul 2026 15:17:12 +0530 Subject: [PATCH 02/14] SK-3005 added update by skyflow id flowdb support --- .../collect/FlowVaultCollectAPICallback.swift | 45 ++++++++-------- .../collect/FlowVaultInsertAPICallback.swift | 45 ++++++++-------- .../collect/FlowVaultUpdateRequestBody.swift | 4 +- .../skyflow_iOS_collectUtilTests.swift | 51 +++++++++++-------- .../skyflow_iOS_insertUtilTests 2.swift | 33 +++++++----- 5 files changed, 96 insertions(+), 82 deletions(-) diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift index 9bcb1c42..292d7446 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -24,12 +24,12 @@ internal class FlowVaultCollectAPICallback: Callback { internal func onSuccess(_ responseBody: Any) { let insertRecords = records["records"] as? [[String: Any]] ?? [] - let updatesByTable = self.groupUpdatesByTable(records["update"] as? [String: Any] ?? [:]) + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) let hasInsert = !insertRecords.isEmpty - let hasUpdate = !updatesByTable.isEmpty + let hasUpdate = !updateRecords.isEmpty if !hasInsert && !hasUpdate { - self.callback.onSuccess(["records": [], "errors": []]) + self.callback.onSuccess(["records": []]) return } @@ -51,7 +51,7 @@ internal class FlowVaultCollectAPICallback: Callback { } let errors = response["errors"] as? [[String: Any]] ?? [] if errors.isEmpty { - self.callback.onSuccess(response) + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) } else { self.callback.onFailure(response) } @@ -101,11 +101,11 @@ internal class FlowVaultCollectAPICallback: Callback { } } - for (tableName, tableRecords) in updatesByTable { + if hasUpdate { group.enter() let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! do { - let (request, session) = try self.getUpdateRequestSession(url: url, tableName: tableName, records: tableRecords) + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) let task = session.dataTask(with: request) { data, response, error in defer { group.leave() } do { @@ -128,11 +128,10 @@ internal class FlowVaultCollectAPICallback: Callback { } group.notify(queue: .main) { - let response: [String: Any] = ["records": mergedRecords, "errors": mergedErrors] if mergedErrors.isEmpty { - self.callback.onSuccess(response) + self.callback.onSuccess(["records": mergedRecords]) } else { - self.callback.onFailure(response) + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) } } } @@ -141,16 +140,15 @@ internal class FlowVaultCollectAPICallback: Callback { self.callback.onFailure(error) } - internal func groupUpdatesByTable(_ updateDict: [String: Any]) -> [String: [[String: Any]]] { - var updatesByTable: [String: [[String: Any]]] = [:] + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] for (skyflowID, value) in updateDict { guard let entry = value as? [String: Any], let tableName = entry["table"] as? String, let fields = entry["fields"] as? [String: Any] else { continue } - let record: [String: Any] = ["skyflowID": skyflowID, "data": fields] - updatesByTable[tableName, default: []].append(record) + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) } - return updatesByTable + return updateRecords } internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { @@ -191,7 +189,7 @@ internal class FlowVaultCollectAPICallback: Callback { } - internal func getUpdateRequestSession(url: URL, tableName: String, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { var jsonString = "" do { @@ -205,7 +203,7 @@ internal class FlowVaultCollectAPICallback: Callback { request.httpMethod = "POST" do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, tableName: tableName, records: records)) + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) request.httpBody = data } @@ -274,15 +272,16 @@ internal class FlowVaultCollectAPICallback: Callback { if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } errorRecords.append(errorEntry) } else { - var successEntry: [String: Any] = [:] - if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } - if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } - if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } - if let data = entry["data"] as? [String: Any] { successEntry["data"] = self.buildFieldsDict(dict: data) } - if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + var fields: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { fields["skyflow_id"] = skyflowID } if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { - successEntry["tokens"] = self.buildFieldsDict(dict: tokens) + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } } + var successEntry: [String: Any] = ["fields": fields] + if let tableName = entry["tableName"] { successEntry["table"] = tableName } + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } successRecords.append(successEntry) } } diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift index df88e01a..131adce1 100644 --- a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -23,12 +23,12 @@ internal class FlowVaultInsertAPICallback: Callback { } internal func onSuccess(_ responseBody: Any) { let insertRecords = records["records"] as? [[String: Any]] ?? [] - let updatesByTable = self.groupUpdatesByTable(records["update"] as? [String: Any] ?? [:]) + let updateRecords = self.flattenUpdates(records["update"] as? [String: Any] ?? [:]) let hasInsert = !insertRecords.isEmpty - let hasUpdate = !updatesByTable.isEmpty + let hasUpdate = !updateRecords.isEmpty if !hasInsert && !hasUpdate { - self.callback.onSuccess(["records": [], "errors": []]) + self.callback.onSuccess(["records": []]) return } @@ -46,7 +46,7 @@ internal class FlowVaultInsertAPICallback: Callback { let response = try self.processResponse(data: data, response: response, error: error) let errors = response["errors"] as? [[String: Any]] ?? [] if errors.isEmpty { - self.callback.onSuccess(response) + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) } else { self.callback.onFailure(response) } @@ -92,11 +92,11 @@ internal class FlowVaultInsertAPICallback: Callback { } } - for (tableName, tableRecords) in updatesByTable { + if hasUpdate { group.enter() let url = URL(string: self.apiClient.vaultURL + "v2/records/update")! do { - let (request, session) = try self.getUpdateRequestSession(url: url, tableName: tableName, records: tableRecords) + let (request, session) = try self.getUpdateRequestSession(url: url, records: updateRecords) let task = session.dataTask(with: request) { data, response, error in defer { group.leave() } do { @@ -115,11 +115,10 @@ internal class FlowVaultInsertAPICallback: Callback { } group.notify(queue: .main) { - let response: [String: Any] = ["records": mergedRecords, "errors": mergedErrors] if mergedErrors.isEmpty { - self.callback.onSuccess(response) + self.callback.onSuccess(["records": mergedRecords]) } else { - self.callback.onFailure(response) + self.callback.onFailure(["records": mergedRecords, "errors": mergedErrors]) } } } @@ -128,16 +127,15 @@ internal class FlowVaultInsertAPICallback: Callback { self.callback.onFailure(error) } - internal func groupUpdatesByTable(_ updateDict: [String: Any]) -> [String: [[String: Any]]] { - var updatesByTable: [String: [[String: Any]]] = [:] + internal func flattenUpdates(_ updateDict: [String: Any]) -> [[String: Any]] { + var updateRecords: [[String: Any]] = [] for (skyflowID, value) in updateDict { guard let entry = value as? [String: Any], let tableName = entry["table"] as? String, let fields = entry["fields"] as? [String: Any] else { continue } - let record: [String: Any] = ["skyflowID": skyflowID, "data": fields] - updatesByTable[tableName, default: []].append(record) + updateRecords.append(["skyflowID": skyflowID, "tableName": tableName, "data": fields]) } - return updatesByTable + return updateRecords } internal func buildFieldsDict(dict: [String: Any]) -> [String: Any] { @@ -178,7 +176,7 @@ internal class FlowVaultInsertAPICallback: Callback { } - internal func getUpdateRequestSession(url: URL, tableName: String, records: [[String: Any]]) throws -> (URLRequest, URLSession) { + internal func getUpdateRequestSession(url: URL, records: [[String: Any]]) throws -> (URLRequest, URLSession) { var jsonString = "" do { @@ -192,7 +190,7 @@ internal class FlowVaultInsertAPICallback: Callback { request.httpMethod = "POST" do { - let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, tableName: tableName, records: records)) + let data = try JSONSerialization.data(withJSONObject: FlowVaultUpdateRequestBody.createRequestBody(vaultID: self.apiClient.vaultID, records: records)) request.httpBody = data } @@ -262,15 +260,16 @@ internal class FlowVaultInsertAPICallback: Callback { if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } errorRecords.append(errorEntry) } else { - var successEntry: [String: Any] = [:] - if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } - if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } - if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } - if let data = entry["data"] as? [String: Any] { successEntry["data"] = self.buildFieldsDict(dict: data) } - if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + var fields: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { fields["skyflow_id"] = skyflowID } if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { - successEntry["tokens"] = self.buildFieldsDict(dict: tokens) + for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { + fields[column] = tokenValue + } } + var successEntry: [String: Any] = ["fields": fields] + if let tableName = entry["tableName"] { successEntry["table"] = tableName } + if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } successRecords.append(successEntry) } } diff --git a/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift index ad929c5f..54187810 100644 --- a/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift +++ b/Sources/Skyflow/collect/FlowVaultUpdateRequestBody.swift @@ -7,7 +7,7 @@ import Foundation internal class FlowVaultUpdateRequestBody { - internal static func createRequestBody(vaultID: String, tableName: String, records: [[String: Any]]) -> [String: Any] { - return ["vaultID": vaultID, "tableName": tableName, "records": records] + internal static func createRequestBody(vaultID: String, records: [[String: Any]]) -> [String: Any] { + return ["vaultID": vaultID, "records": records] } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 1dadd6b2..f269f489 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -68,9 +68,10 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let errors = result["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") - XCTAssertEqual(records[0]["tableName"] as! String, "table") - XCTAssertNil(records[0]["tokens"]) + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") + XCTAssertNil(fields["tokens"]) XCTAssertTrue(errors.isEmpty) } catch { XCTFail(error.localizedDescription) @@ -78,7 +79,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { } func testGetCollectResponseWithTokens() { - let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] as [String: Any] + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] self.collectCallback.options = FlowVaultICOptions() do { @@ -87,14 +88,17 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["tableName"] as! String, "table") - XCTAssertEqual(records[0]["tokens"] as! [String: String], ["field": "value"]) + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { XCTFail(error.localizedDescription) } } - func testGetCollectResponseWithDataAndHashedDataAndHttpCode() { + func testGetCollectResponseWithHashedData() { let response: [String: Any] = ["records": [[ "skyflowID": "SID", "tableName": "table", @@ -110,8 +114,9 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["httpCode"] as? Int, 200) - XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) + XCTAssertNil(records[0]["httpCode"]) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) } catch { XCTFail(error.localizedDescription) @@ -127,8 +132,9 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) let records = processedData["records"] as! [[String: Any]] - XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") - XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") } catch { XCTFail(error.localizedDescription) @@ -245,9 +251,10 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let errors = processedInsert["errors"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") - let tokens = insertRecords[0]["tokens"] as? [String: String] - XCTAssertEqual(tokens?["field"], "value") + XCTAssertEqual(insertRecords[0]["table"] as? String, "table") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as? String, "SID") + XCTAssertEqual(fields["field"] as? String, "value") XCTAssertTrue(errors.isEmpty) } catch { XCTFail("Insert scenario failed: \(error)") @@ -368,7 +375,8 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let errors = processed["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { @@ -407,11 +415,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTAssertTrue(errors.isEmpty) XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["skyflowID"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") - let data = records[0]["data"] as! [String: Any] - XCTAssertEqual(data["name"] as? String, "sagar") - let tokens = records[0]["tokens"] as! [String: Any] - let nameTokens = tokens["name"] as! [[String: Any]] + XCTAssertEqual(records[0]["table"] as? String, "table1") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") + XCTAssertNil(fields["data"]) + let nameTokens = fields["name"] as! [[String: Any]] XCTAssertEqual(nameTokens[0]["token"] as? String, "sagwm") } catch { XCTFail("Update success response should not throw: \(error)") @@ -486,7 +494,8 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let errors = processed["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) } catch { diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index ba71c627..8c145452 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -67,8 +67,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let errors = result["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") - XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") XCTAssertTrue(errors.isEmpty) } catch { XCTFail(error.localizedDescription) @@ -76,7 +77,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { } func testGetCollectResponseWithTokens() { - let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": "value"]]]] as [String: Any] + let response = ["records": [["skyflowID": "SID", "tableName": "table", "tokens": ["field": [["token": "tok", "tokenGroupName": "group"]]]]]] as [String: Any] self.collectCallback.options = FlowVaultICOptions() do { @@ -85,14 +86,17 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["tableName"] as! String, "table") - XCTAssertEqual(records[0]["tokens"] as! [String: String], ["field": "value"]) + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") + let fieldTokens = fields["field"] as! [[String: Any]] + XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { XCTFail(error.localizedDescription) } } - func testGetCollectResponseWithDataAndHashedDataAndHttpCode() { + func testGetCollectResponseWithHashedData() { let response: [String: Any] = ["records": [[ "skyflowID": "SID", "tableName": "table", @@ -108,8 +112,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["httpCode"] as? Int, 200) - XCTAssertEqual(records[0]["data"] as! [String: String], ["field": "value"]) + XCTAssertNil(records[0]["httpCode"]) + let fields = records[0]["fields"] as! [String: Any] + XCTAssertNil(fields["data"]) XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) } catch { XCTFail(error.localizedDescription) @@ -125,8 +130,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) let records = processedData["records"] as! [[String: Any]] - XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") - XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["table"] as! String, "table") + let fields = records[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as! String, "SID") } catch { XCTFail(error.localizedDescription) @@ -238,9 +244,10 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let insertRecords = processedInsert["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") - let tokens = insertRecords[0]["tokens"] as? [String: String] - XCTAssertEqual(tokens?["field"], "value") + XCTAssertEqual(insertRecords[0]["table"] as? String, "table") + let fields = insertRecords[0]["fields"] as! [String: Any] + XCTAssertEqual(fields["skyflow_id"] as? String, "SID") + XCTAssertEqual(fields["field"] as? String, "value") } catch { XCTFail("Insert scenario failed: \(error)") } From 8d17511f90ec5511ba2a8d395197a7774f537251 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 22 Jul 2026 17:26:18 +0530 Subject: [PATCH 03/14] SK-3005 added reveal flowdb support --- .../FlowVaultDetokenizeRequestBody.swift | 10 +++- .../reveal/FlowVaultRevealApiCallback.swift | 9 +++ Sources/Skyflow/reveal/RevealContainer.swift | 9 ++- .../Skyflow/reveal/RevealElementInput.swift | 6 +- .../skyflow_iOS_revealTests.swift | 16 ++++++ .../skyflow_iOS_revealUtilTests.swift | 57 +++++++++++++++++++ 6 files changed, 104 insertions(+), 3 deletions(-) diff --git a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift index f6c7bef4..755d47de 100644 --- a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift +++ b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift @@ -11,7 +11,15 @@ internal class FlowVaultDetokenizeRequestBody { let tokens = records.map { $0.token } var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { - body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } + // Dedupe by tokenGroupName, keeping the last occurrence so explicitly-passed + // options can override anything derived from individual reveal elements. + var redactionByGroup: [String: String] = [:] + var order: [String] = [] + for tgr in tokenGroupRedactions { + if redactionByGroup[tgr.tokenGroupName] == nil { order.append(tgr.tokenGroupName) } + redactionByGroup[tgr.tokenGroupName] = tgr.redaction + } + body["tokenGroupRedactions"] = order.map { ["tokenGroupName": $0, "redaction": redactionByGroup[$0]!] } } return body } diff --git a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift index 48a21243..164343f6 100644 --- a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift +++ b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift @@ -100,6 +100,15 @@ class FlowVaultRevealAPICallback: Callback { if let httpResponse = response as? HTTPURLResponse { let range = 400...599 if range ~= httpResponse.statusCode { + // FlowDB returns a non-2xx status when every token in the batch fails to + // detokenize, but the body still has the same {"response": [...]} shape as a + // success/partial response (each entry carrying its own error/httpCode) - parse + // it as such instead of collapsing into a generic top-level error. + if let safeData = data, + let jsonObject = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + jsonObject["response"] != nil { + return try getDetokenizeResponseBody(data: safeData) + } var description = "Detokenize call failed with the following status code " + String(httpResponse.statusCode) if let safeData = data { do { diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index aa426ff3..3afff29c 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -65,6 +65,13 @@ public extension Container { list.append(RevealRequestRecord(token: id)) } } + var tokenGroupRedactions: [TokenGroupRedaction] = [] + for element in self.revealElements { + if let tokenGroupName = element.revealInput.tokenGroupName, let redaction = element.revealInput.redaction { + tokenGroupRedactions.append(TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + tokenGroupRedactions.append(contentsOf: options?.tokenGroupRedactions ?? []) let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, onSuccessHandler: { Log.info(message: .REVEAL_SUBMIT_SUCCESS, contextOptions: tempContextOptions) @@ -72,7 +79,7 @@ public extension Container { onFailureHandler: { } ) - self.skyflow.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + self.skyflow.apiClient.get(records: list, tokenGroupRedactions: tokenGroupRedactions.isEmpty ? nil : tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } } } diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/reveal/RevealElementInput.swift index 3bf4d7f3..7138b857 100644 --- a/Sources/Skyflow/reveal/RevealElementInput.swift +++ b/Sources/Skyflow/reveal/RevealElementInput.swift @@ -13,13 +13,17 @@ public struct RevealElementInput { internal var errorTextStyles: Styles? internal var label: String internal var altText: String + internal var redaction: String? + internal var tokenGroupName: String? - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { + public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "", redaction: String? = nil, tokenGroupName: String? = nil) { self.token = token self.inputStyles = inputStyles self.labelStyles = labelStyles self.errorTextStyles = errorTextStyles self.label = label self.altText = altText + self.redaction = redaction + self.tokenGroupName = tokenGroupName } } diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 2905454a..b1445e4b 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -59,6 +59,22 @@ class skyflow_iOS_revealTests: XCTestCase { XCTAssertEqual(revealElementInput.label, "RevealElement") } + func testRevealElementInputWithRedactionAndTokenGroupName() { + let revealElementInput = RevealElementInput(token: revealTestId, label: "RevealElement", redaction: "MASKED", tokenGroupName: "deterministic_string") + + XCTAssertEqual(revealElementInput.redaction, "MASKED") + XCTAssertEqual(revealElementInput.tokenGroupName, "deterministic_string") + } + + func testRevealElementCarriesRedactionAndTokenGroupNameThroughContainer() { + let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) + let revealElementInput = RevealElementInput(token: revealTestId, label: "RevealElement", redaction: "PLAIN_TEXT", tokenGroupName: "nondeterministic_string") + _ = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) + + XCTAssertEqual(revealContainer?.revealElements[0].revealInput.redaction, "PLAIN_TEXT") + XCTAssertEqual(revealContainer?.revealElements[0].revealInput.tokenGroupName, "nondeterministic_string") + } + func testCreateSkyflowRevealContainer() { let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) let revealElementInput = getRevealElementInput() diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index 0a9b4df6..45402d56 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -165,6 +165,63 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { } } + func testProcessResponseFullFailureWithNon2xxOuterStatus() { + let revealedResponse: [String: Any] = ["response": [ + ["token": "dedwimm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token dedwimm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()], + ["token": "femwdmm", "value": NSNull(), "tokenGroupName": NSNull(), + "error": "Detokenize failed. Token femwdmm is invalid. Specify a valid token.", + "httpCode": 404, "metadata": NSNull()] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + let records = response["records"] as! [[String: Any]] + let errors = response["errors"] as! [[String: Any]] + + XCTAssertTrue(records.isEmpty) + XCTAssertEqual(errors.count, 2) + XCTAssertEqual(errors[0]["error"] as? String, "Detokenize failed. Token dedwimm is invalid. Specify a valid token.") + XCTAssertEqual(errors[0]["httpCode"] as? Int, 404) + } catch { + XCTFail("Full failure with 404 outer status should not throw: \(error)") + } + } + + func testProcessResponseInvalidTokenGroupError() { + let revealedResponse: [String: Any] = ["error": [ + "grpc_code": 3, + "http_code": 400, + "message": "Detokenize failed. Token group no is invalid. Specify a valid token group.", + "http_status": "Bad Request", + "details": [] + ]] + let httpResponse = HTTPURLResponse(url: URL(string: "https://www.example.org")!, statusCode: 400, httpVersion: "1.1", headerFields: nil) + do { + let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) + _ = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) + XCTFail("Should throw on genuine top-level error (invalid token group)") + } catch { + XCTAssertEqual(error.localizedDescription, "Detokenize failed. Token group no is invalid. Specify a valid token group.") + } + } + + func testConstructV2DetokenizeRequestBodyDedupesTokenGroupRedactions() { + let records = [RevealRequestRecord(token: "token1")] + let tokenGroupRedactions = [ + TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED"), + TokenGroupRedaction(tokenGroupName: "group1", redaction: "PLAIN_TEXT") + ] + let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) + + let redactions = result["tokenGroupRedactions"] as! [[String: Any]] + XCTAssertEqual(redactions.count, 1) + XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[0]["redaction"] as? String, "PLAIN_TEXT") + } + func testGetTokensToErrors() { let errors = [["token": "1234"], ["token": "4321"]] From 2209d088c227af70c9f1bb8ffa8c2549151fa20e Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 23 Jul 2026 23:56:52 +0530 Subject: [PATCH 04/14] SK-3005 added reveal flowdb support --- .../collect/FlowVaultCollectAPICallback.swift | 21 ++- .../collect/FlowVaultInsertAPICallback.swift | 15 +- Sources/Skyflow/core/Client.swift | 10 +- Sources/Skyflow/reveal/RevealContainer.swift | 1 - Sources/Skyflow/reveal/RevealOptions.swift | 6 +- .../Skyflow/reveal/RevealValueCallback.swift | 45 ++---- .../skyflow_iOS_collectUtilTests.swift | 26 +++- .../skyflow_iOS_insertUtilTests 2.swift | 23 ++- .../skyflow_iOS_revealUtilTests.swift | 142 ++++++++++++++++++ 9 files changed, 214 insertions(+), 75 deletions(-) diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift index 292d7446..3eae2ca3 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -233,18 +233,16 @@ internal class FlowVaultCollectAPICallback: Callback { } var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) if let safeData = data { - do { - let errorResponse = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] - if let errorDetails = errorResponse["error"] as? [String: Any], - let message = errorDetails["message"] as? String { - description = message - } - if let requestId = httpResponse.allHeaderFields["x-request-id"] { - description += " - request-id: \(requestId)" - } - } catch { + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "code": httpResponse.statusCode]] } + if let errorDetails = errorResponse["error"] as? [String: Any], + let message = errorDetails["message"] as? String { + description = message + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } } return ["error": ["message": description, "code": httpResponse.statusCode]] } @@ -259,7 +257,7 @@ internal class FlowVaultCollectAPICallback: Callback { } func getCollectResponseBody(data: Data) throws -> [String: Any]{ - let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] var successRecords: [[String: Any]] = [] var errorRecords: [[String: Any]] = [] @@ -282,6 +280,7 @@ internal class FlowVaultCollectAPICallback: Callback { var successEntry: [String: Any] = ["fields": fields] if let tableName = entry["tableName"] { successEntry["table"] = tableName } if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } successRecords.append(successEntry) } } diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift index 131adce1..633b45ef 100644 --- a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -222,16 +222,16 @@ internal class FlowVaultInsertAPICallback: Callback { var errorObject: Error = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) if let safeData = data { - do { - let desc = try JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as! [String: Any] - let error = desc["error"] as! [String: Any] - description = error["message"] as! String + if let desc = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], + let errorDict = desc["error"] as? [String: Any], + let message = errorDict["message"] as? String { + description = message if let requestId = httpResponse.allHeaderFields["x-request-id"] { description += " - request-id: \(requestId)" } errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) - } catch { - errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8)!).getErrorObject(contextOptions: self.contextOptions) + } else { + errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8) ?? "Unknown error").getErrorObject(contextOptions: self.contextOptions) } } throw errorObject @@ -247,7 +247,7 @@ internal class FlowVaultInsertAPICallback: Callback { } func getCollectResponseBody(data: Data) throws -> [String: Any]{ - let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] + let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] var successRecords: [[String: Any]] = [] var errorRecords: [[String: Any]] = [] @@ -270,6 +270,7 @@ internal class FlowVaultInsertAPICallback: Callback { var successEntry: [String: Any] = ["fields": fields] if let tableName = entry["tableName"] { successEntry["table"] = tableName } if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } + if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } successRecords.append(successEntry) } } diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index 81c1a379..0938da71 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -22,7 +22,7 @@ public class Client { Log.info(message: .CLIENT_INITIALIZED, contextOptions: self.contextOptions) } - public func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { + internal func insert(records: [String: Any], options: InsertOptions = InsertOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .INSERT Log.info(message: .INSERT_TRIGGERED, contextOptions: tempContextOptions) @@ -115,7 +115,7 @@ public class Client { return nil } - public func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { + internal func detokenize(records: [String: Any], options: RevealOptions? = RevealOptions(), callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .DETOKENIZE func checkRecord(token: [String: Any], index: Int) -> ErrorCodes? { @@ -164,13 +164,13 @@ public class Client { onFailureHandler: { } ) - self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) } else { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } } - public func getById(records: [String: Any], callback: Callback) { + internal func getById(records: [String: Any], callback: Callback) { var tempContextOptions = self.contextOptions tempContextOptions.interface = .GETBYID Log.info(message: .GET_BY_ID_TRIGGERED, contextOptions: tempContextOptions) @@ -253,7 +253,7 @@ public class Client { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } } - public func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ + internal func get(records: [String: Any], options: GetOptions = GetOptions(), callback: Callback){ var tempContextOptions = self.contextOptions tempContextOptions.interface = .GET Log.info(message: .GET_TRIGGERED, contextOptions: tempContextOptions) diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index 3afff29c..c7254c25 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -71,7 +71,6 @@ public extension Container { tokenGroupRedactions.append(TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) } } - tokenGroupRedactions.append(contentsOf: options?.tokenGroupRedactions ?? []) let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, onSuccessHandler: { Log.info(message: .REVEAL_SUBMIT_SUCCESS, contextOptions: tempContextOptions) diff --git a/Sources/Skyflow/reveal/RevealOptions.swift b/Sources/Skyflow/reveal/RevealOptions.swift index 1f63a9e4..18e1d33c 100644 --- a/Sources/Skyflow/reveal/RevealOptions.swift +++ b/Sources/Skyflow/reveal/RevealOptions.swift @@ -12,9 +12,5 @@ import Foundation public struct RevealOptions { - public var tokenGroupRedactions: [TokenGroupRedaction]? - - public init(tokenGroupRedactions: [TokenGroupRedaction]? = nil) { - self.tokenGroupRedactions = tokenGroupRedactions - } + public init() {} } diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Sources/Skyflow/reveal/RevealValueCallback.swift index a32438fc..ac9803ec 100644 --- a/Sources/Skyflow/reveal/RevealValueCallback.swift +++ b/Sources/Skyflow/reveal/RevealValueCallback.swift @@ -18,28 +18,17 @@ internal class RevealValueCallback: Callback { func onSuccess(_ responseBody: Any) { var tokens: [String: String] = [:] - - let responseJson = responseBody as! [String: Any] + let responseJson = responseBody as? [String: Any] ?? [:] var response: [String: Any] = [:] - var tempSuccessResponses: [[String: String]] = [] + var successResponses: [[String: Any]] = [] if let records = responseJson["records"] as? [Any] { for record in records { - let dict = record as! [String: Any] - let token = dict["token"] as! String + guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token - - var successEntry: [String: String] = [:] - successEntry["token"] = token - tempSuccessResponses.append(successEntry) - } - } - - var successResponses: [[String: String]] = [] - for entry in tempSuccessResponses { - if let token = entry["token"] { - successResponses.append(entry) + + successResponses.append(dict) } } @@ -77,36 +66,24 @@ internal class RevealValueCallback: Callback { func onFailure(_ error: Any) { var response: [String: Any] = [:] - if error is [String: Any] { + if let responseJson = error as? [String: Any] { var tokens: [String: String] = [:] - - let responseJson = error as! [String: Any] - var tempSuccessResponses: [[String: String]] = [] + var successResponses: [[String: Any]] = [] if let records = responseJson["records"] as? [Any] { for record in records { - let dict = record as! [String: Any] - let token = dict["token"] as! String + guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token - - var successEntry: [String: String] = [:] - successEntry["token"] = token - tempSuccessResponses.append(successEntry) - } - } - - var successResponses: [[String: String]] = [] - for entry in tempSuccessResponses { - if let token = entry["token"] { - successResponses.append(entry) + + successResponses.append(dict) } } if successResponses.count != 0 { response["success"] = successResponses } - var errors = [[:]] as [[String: Any]] + var errors = [] as [[String: Any]] if let responseErrors = responseJson["errors"] as? [[String: Any]] { errors = responseErrors } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index f269f489..3115045f 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -114,7 +114,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertNil(records[0]["httpCode"]) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) let fields = records[0]["fields"] as! [String: Any] XCTAssertNil(fields["data"]) XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) @@ -142,15 +142,29 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { } func testProcessResponseError() { - let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } - let processedData = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) - as! [String: [String: String]] + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) + XCTAssertEqual((result["errors"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index 8c145452..fb828838 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -112,7 +112,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertNil(records[0]["httpCode"]) + XCTAssertEqual(records[0]["httpCode"] as? Int, 200) let fields = records[0]["fields"] as! [String: Any] XCTAssertNil(fields["data"]) XCTAssertEqual(records[0]["hashedData"] as! [String: String], ["field": "hashed-value"]) @@ -140,16 +140,27 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { } func testProcessResponseError() { - let response = ["records": [["skyflowID": "SID", "tableName": "table"]]] + // A genuine connection/network-level error (URLSession error, no HTTP response reached). + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) do { - let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) - let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - - _ = try self.collectCallback.processResponse(data: data, response: response, error: NSError(domain: "", code: 400, userInfo: nil)) + _ = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) XCTFail("Should have thrown error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + func testGetCollectResponseBodyWithMalformedTopLevelJSON() { + // A response body that parses as valid JSON but isn't a top-level object (e.g. a bare + // array or scalar) must not crash - it should fall back to an empty result. + do { + let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) + let result = try self.collectCallback.getCollectResponseBody(data: arrayData) + XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) + XCTAssertEqual((result["errors"] as? [[String: Any]])?.count, 0) } catch { + XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index 45402d56..fefa739a 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -231,6 +231,148 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { XCTAssertEqual(result["4321"], "Invalid Token") } + func testRevealValueOnSuccessPureSuccess() { + // Every token revealed, zero errors - a genuine full-success scenario + // (as opposed to the mixed success+error fixtures used elsewhere in this file). + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [ + ["token": token1, "value": "John"], + ["token": token2, "value": "Doe"] + ], + "errors": [] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first"), options: RevealElementOptions()) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second"), options: RevealElementOptions()) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["errors"]) + let records = self.callback.data["success"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(element1.actualValue, "John") + XCTAssertEqual(element2.actualValue, "Doe") + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element2.errorMessage.text, nil) + } + + func testRevealValueOnFailurePureFailure() { + // Every token invalid, zero successes - a genuine full-failure scenario. + let token1 = "123" + let token2 = "456" + let response: [String: Any] = [ + "records": [], + "errors": [ + ["token": token1, "error": "Invalid Token"], + ["token": token2, "error": "Invalid Token"] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + let element2 = self.container.create(input: RevealElementInput(token: token2, label: "second")) + + self.revealValueCallback.revealElements = [element1, element2] + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["success"]) + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 2) + XCTAssertEqual(element1.actualValue, nil) + XCTAssertEqual(element2.actualValue, nil) + XCTAssertEqual(element1.errorMessage.text, "Invalid Token") + XCTAssertEqual(element2.errorMessage.text, "Invalid Token") + } + + func testRevealValueOnFailureNetworkErrorHasNoPerTokenErrors() { + // Simulates what FlowVaultRevealAPICallback.callRevealOnFailure produces for a genuine + // network/connection-level failure: no "records", and the error entry has no "token" + // (since the failure isn't scoped to any specific token). Elements should not crash and + // should not show a per-element error message (there's no token to match against), but + // the client callback should still receive the error. + let token1 = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let response: [String: Any] = [ + "errors": [["error": networkError]] + ] + + let element1 = self.container.create(input: RevealElementInput(token: token1, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["success"]) + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual((errors[0]["error"] as? NSError)?.code, -1009) + // No token to match this error against, so the element shows no inline error. + XCTAssertEqual(element1.errorMessage.text, nil) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnFailureWithNonDictionaryError() { + // onFailure(_ error: Any) can in principle be called with something that isn't a + // dictionary at all (the Callback protocol accepts Any). Must not crash, and should + // report an empty response rather than propagating garbage. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure("not a dictionary") + wait(for: [self.expectation], timeout: 20.0) + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + } + + func testRevealValueOnSuccessWithNonDictionaryResponseBody() { + // Same defensive case for onSuccess: a non-dictionary responseBody must not crash. + let element1 = self.container.create(input: RevealElementInput(token: "123", label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onSuccess(42) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["success"]) + XCTAssertNil(self.callback.data["errors"]) + XCTAssertEqual(element1.actualValue, nil) + } + + func testRevealValueOnSuccessSkipsMalformedRecords() { + // A record missing "token" (or not a dictionary at all) must be skipped, not crash + // the whole reveal for every other token in the same batch. + let goodToken = "123" + let response: [String: Any] = [ + "records": [ + ["token": goodToken, "value": "John"], + ["value": "no token here"], + "not even a dictionary", + ["token": 42, "value": "token is not a String"] + ], + "errors": [] + ] + + let goodElement = self.container.create(input: RevealElementInput(token: goodToken, label: "good"), options: RevealElementOptions()) + self.revealValueCallback.revealElements = [goodElement] + + self.revealValueCallback.onSuccess(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + let records = self.callback.data["success"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, goodToken) + XCTAssertEqual(goodElement.actualValue, "John") + } + func testRevealValueOnFailure() { let successToken = "123" let failureToken = "1234" From 02807b14053bc855f7c6b76b1e6fb71b454ae729 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Fri, 24 Jul 2026 10:12:59 +0530 Subject: [PATCH 05/14] SK-3005 added unit test cases --- ...skyflow_iOS_composableEelementsTests.swift | 13 + .../Skyflow_iOS_collectErrorTests.swift | 11 + .../Skyflow_iOS_revealErrorTests.swift | 27 ++ .../skyflow_iOS_getByIdTests.swift | 17 +- .../skyflow_iOS_getTests.swift | 18 +- ...w_iOS_dormantCollectApiCallbackTests.swift | 249 ++++++++++++++++++ ...w_iOS_dormantCollectRequestBodyTests.swift | 102 +++++++ .../skyflow_iOS_revealApiCallbackTests.swift | 178 +++++++++++++ .../skyflow_iOS_utilTests.swift | 139 ++++++++++ 9 files changed, 751 insertions(+), 3 deletions(-) create mode 100644 Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift create mode 100644 Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift create mode 100644 Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index be69c6a3..a92a171d 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -485,6 +485,19 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) } + + func testComposableCollectEmptyVaultURL() { + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let container = clientWithEmptyURL.container(type: ContainerType.COMPOSABLE) + + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + container?.collect(callback: callback) + + wait(for: [expectation], timeout: 20.0) + + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) + } func testCreateRows() { let elements = [1, 2] let numberOfRows = 2 diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 4c5902fe..43df5994 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -432,4 +432,15 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) } + func testInsertEmptyVaultURL() { + let expectation = XCTestExpectation(description: "Insert with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.insert(records: ["records": [["table": "table", "fields": ["field": "value"]]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .INSERT)).localizedDescription) + } + } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index cce57190..075157ec 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -115,4 +115,31 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { XCTAssertEqual(result, ErrorCodes.EMPTY_TOKEN_ID().description) } + func testContainerRevealEmptyVaultURL() { + // Unlike Client.detokenize()/getById()/get(), RevealContainer.reveal() calls + // callback.onFailure directly with the raw NSError (no callRevealOnFailure wrapping). + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + let revealContainer = clientWithEmptyURL.container(type: ContainerType.REVEAL, options: nil) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Reveal with empty vaultURL should fail")) + revealContainer?.reveal(callback: callback) + + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .REVEAL_CONTAINER)).localizedDescription) + } + + func testDetokenizeEmptyVaultURL() { + // Client.detokenize()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "Detokenize with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.detokenize(records: ["records": [["token": "sometoken"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift index 315d8832..00c9fcd9 100644 --- a/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift +++ b/Tests/skyflow-iOS-getByIdTests/skyflow_iOS_getByIdTests.swift @@ -74,5 +74,20 @@ class skyflow_iOS_getByIdTests: XCTestCase { XCTAssertEqual(errorMessage, "TokenProvider error") } - + + func testGetByIdEmptyVaultURL() { + // Client.getById()'s vault-level errors route through callRevealOnFailure, which + // wraps the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "getById with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.getById(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift index eabf5a0b..a0413337 100644 --- a/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift +++ b/Tests/skyflow-iOS-getTests/skyflow_iOS_getTests.swift @@ -108,6 +108,20 @@ class skyflow_iOS_getTests: XCTestCase { print(callback.receivedResponse) XCTAssertTrue(callback.data.description.contains(ErrorCodes.REDACTION_WITH_TOKEN_NOT_SUPPORTED().description)) } - - + + func testGetEmptyVaultURL() { + // Client.get()'s vault-level errors route through callRevealOnFailure, which wraps + // the NSError in {"errors": [errorObject]} rather than passing it through raw. + let expectation = XCTestExpectation(description: "get with empty vaultURL should fail") + let callback = DemoAPICallback(expectation: expectation) + let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) + + clientWithEmptyURL.get(records: ["records": [["ids": ["id1"], "table": "persons"]]], callback: callback) + + wait(for: [expectation], timeout: 10.0) + let errors = callback.data["errors"] as! [NSError] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0].localizedDescription, ErrorCodes.EMPTY_VAULT_URL().description) + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift new file mode 100644 index 00000000..9da2f33e --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectApiCallbackTests.swift @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectAPICallback/InsertAPICallback (kept for potential +// future PDB reuse). + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_dormantCollectApiCallbackTests: XCTestCase { + var collectCallback: CollectAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.collectCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.collectCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testOnSuccessWithNoInsertAndNoUpdateRecords() { + // No network call should be made at all when there's nothing to insert or update. + let expectation = XCTestExpectation(description: "Empty batch should succeed with an empty response") + let callback = DemoAPICallback(expectation: expectation) + let emptyCallback = CollectAPICallback( + callback: callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": []], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + + emptyCallback.onSuccess("token") + wait(for: [expectation], timeout: 10.0) + + XCTAssertTrue(callback.data.isEmpty) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.collectCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + let result = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "offline") + } catch { + XCTFail("Should not throw for a connection-level error: \(error)") + } + } + + func testProcessResponseBadStatusCode() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + let result = try self.collectCallback.processResponse(data: data, response: httpResponse, error: nil) + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Internal Server Error - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGetCollectResponseBody() { + // Old v1 gateway-batch response shape: {"responses": [{"records": [{"skyflow_id": ...}]}]} + // getCollectResponseBody walks self.records["records"] (the INPUT records), reading the + // matching entry out of the responses array at the same index - so the input record count + // must match, not the responses array's own length. + let twoRecordCallback = CollectAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: ["records": [["table": "table1", "fields": ["field": "value1"]], ["table": "table2", "fields": ["field": "value2"]]]], + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]], + ["records": [["skyflow_id": "SID2"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try twoRecordCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["table"] as? String, "table1") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[1]["table"] as? String, "table2") + XCTAssertEqual(records[1]["skyflow_id"] as? String, "SID2") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseSuccess() { + let responseDict: [String: Any] = ["skyflow_id": "SID1"] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + XCTAssertEqual(records[0]["table"] as? String, "table") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessUpdateResponseBadStatusCode() { + let responseDict = ["error": ["message": "Record not found"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: nil) + let result = try self.collectCallback.processUpdateResponse(data: data, response: httpResponse, error: nil, table: "table") + let errorDict = result["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "Record not found") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.collectCallback.callback = callback + + self.collectCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} + +final class skyflow_iOS_dormantInsertApiCallbackTests: XCTestCase { + var insertCallback: InsertAPICallback! + var defaultRecord: [String: Any] = ["records": [["table": "table", "fields": ["field": "value"]]]] + + override func setUp() { + self.insertCallback = InsertAPICallback( + callback: DemoAPICallback(expectation: XCTestExpectation()), + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: defaultRecord, + options: ICOptions(tokens: false), + contextOptions: ContextOptions() + ) + } + + func testBuildFieldsDict() { + let dict = ["key": "value", "nested": ["key": "value"]] as [String: Any] + let result = self.insertCallback.buildFieldsDict(dict: dict) + XCTAssertEqual(dict["key"] as! String, result["key"] as! String) + XCTAssertEqual(dict["nested"] as! [String: String], result["nested"] as! [String: String]) + } + + func testGetRequestSession() { + let url = URL(string: "https://example.org")! + do { + let (request, session) = try self.insertCallback.getRequestSession(url: url) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + let body = try JSONSerialization.jsonObject(with: request.httpBody!, options: .allowFragments) as! [String: Any] + let records = body["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["tableName"] as? String, "table") + XCTAssertNotNil(session) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkErrorThrows() { + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + do { + _ = try self.insertCallback.processResponse(data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testProcessResponseBadStatusCodeThrows() { + let responseDict = ["error": ["message": "Internal Server Error"]] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + _ = try self.insertCallback.processResponse(data: data, response: httpResponse, error: nil) + XCTFail("Should throw on a bad status code") + } catch { + XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + } + } + + func testGetCollectResponseBody() { + let responseDict: [String: Any] = [ + "responses": [ + ["records": [["skyflow_id": "SID1"]]] + ] + ] + do { + let data = try JSONSerialization.data(withJSONObject: responseDict, options: .fragmentsAllowed) + let result = try self.insertCallback.getCollectResponseBody(data: data) + let records = result["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "table") + XCTAssertEqual(records[0]["skyflow_id"] as? String, "SID1") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testOnFailure() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + self.insertCallback.callback = callback + + self.insertCallback.onFailure(["errors": [["error": "boom"]]]) + wait(for: [expectation], timeout: 10.0) + + XCTAssertNotNil(callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift new file mode 100644 index 00000000..2b7612f5 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB CollectRequestBody (kept for potential future PDB reuse). +// Distinct from FlowVaultCollectRequestBody, which is the live class covering this same logic. + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_dormantCollectRequestBodyTests: XCTestCase { + var skyflow: Client! + + override func setUp() { + self.skyflow = Client(Configuration(vaultID: "id", vaultURL: "http://demo.com", tokenProvider: DemoTokenProvider(), options: Options(logLevel: .DEBUG))) + } + + func testCreateRequestBodySimpleInsert() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER) + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Pure insert")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as? String, "persons") + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"] + ] + ] + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "table1") + XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) + } + + func testCreateRequestBodyWithSkyflowIDOnElement() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER, skyflowID: "id1") + let cardNumber = container?.create(input: cardNumberInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via element skyflowID")) + let requestBody = CollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + XCTAssertEqual((requestBody?["records"] as! [[String: Any]]).count, 0) + let update = requestBody?["update"] as! [String: Any] + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) + } + + func testCreateRequestBodyDuplicateAdditionalFieldError() { + let additionalFields: [String: Any] = [ + "records": [ + ["table": "table1", "fields": ["column1": "value1"]], + ["table": "table1", "fields": ["column1": "value2"]] + ] + ] + let expectation = XCTestExpectation(description: "Duplicate additional field should fail") + let callback = DemoAPICallback(expectation: expectation) + let requestBody = CollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNil(requestBody) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "column1").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testGetUniqueColumn() { + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] + ] + + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(CollectRequestBody.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift new file mode 100644 index 00000000..c18a05e5 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealApiCallbackTests.swift @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Unit tests for the dormant v1 PDB RevealAPICallback (kept for potential future PDB reuse). + +import XCTest +@testable import Skyflow + +final class skyflow_iOS_revealApiCallbackTests: XCTestCase { + var revealApiCallback: RevealAPICallback! + var expectation: XCTestExpectation! + var callback: DemoAPICallback! + + override func setUp() { + self.expectation = XCTestExpectation() + self.callback = DemoAPICallback(expectation: self.expectation) + self.revealApiCallback = RevealAPICallback( + callback: self.callback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + connectionUrl: "https://example.org/v1/vaults/vault", + records: [RevealRequestRecord(token: "token1")], + contextOptions: ContextOptions() + ) + } + + func testGetRequestSession() { + let (request, _) = self.revealApiCallback.getRequestSession() + + XCTAssertEqual(request.url?.absoluteString, "https://example.org/v1/vaults/vault/detokenize") + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json; utf-8") + XCTAssertEqual(request.allHTTPHeaderFields?["Accept"], "application/json") + XCTAssertEqual(request.allHTTPHeaderFields?["Authorization"], "Bearer ") + } + + func testGetRevealRequestBody() { + do { + let record = RevealRequestRecord(token: "abc123") + let data = try self.revealApiCallback.getRevealRequestBody(record: record) + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + let params = json["detokenizationParameters"] as! [[String: Any]] + + XCTAssertEqual(params.count, 1) + XCTAssertEqual(params[0]["token"] as? String, "abc123") + XCTAssertEqual(params[0]["redaction"] as? String, "PLAIN_TEXT") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseSuccess() { + do { + let record = RevealRequestRecord(token: "abc123") + let responseDict = ["records": [["token": "abc123", "value": "revealed-value"]]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(failure) + XCTAssertEqual(success?.token_id, "abc123") + XCTAssertEqual(success?.value, "revealed-value") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseBadStatusCode() { + do { + let record = RevealRequestRecord(token: "badtoken") + let responseDict = ["error": ["message": "Invalid Token"]] + let data = try JSONSerialization.data(withJSONObject: responseDict) + let httpResponse = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 404, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) + + let (success, failure) = try self.revealApiCallback.processResponse(record: record, data: data, response: httpResponse, error: nil) + + XCTAssertNil(success) + XCTAssertEqual(failure?.id, "badtoken") + XCTAssertEqual(failure?.error.localizedDescription, "Invalid Token - request-id: RID") + } catch { + XCTFail(error.localizedDescription) + } + } + + func testProcessResponseNetworkError() { + let record = RevealRequestRecord(token: "abc123") + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + + do { + _ = try self.revealApiCallback.processResponse(record: record, data: nil, response: nil, error: networkError) + XCTFail("Should throw on a connection-level error") + } catch { + XCTAssertEqual((error as NSError).code, -1009) + } + } + + func testHandleCallbacksAllSuccess() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + + self.revealApiCallback.handleCallbacks(success: success, failure: [], isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["token"] as? String, "t1") + XCTAssertEqual(records[0]["value"] as? String, "v1") + XCTAssertNil(self.callback.data["errors"]) + } + + func testHandleCallbacksPartialFailure() { + let success = [RevealSuccessRecord(token_id: "t1", value: "v1")] + let failure = [RevealErrorRecord(id: "t2", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: success, failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + let records = self.callback.data["records"] as! [[String: Any]] + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["token"] as? String, "t2") + } + + func testHandleCallbacksAllFailure() { + let failure = [RevealErrorRecord(id: "t1", error: NSError(domain: "", code: 404, userInfo: [NSLocalizedDescriptionKey: "Invalid Token"]))] + + self.revealApiCallback.handleCallbacks(success: [], failure: failure, isSuccess: true, errorObject: nil) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNil(self.callback.data["records"]) + let errors = self.callback.data["errors"] as! [[String: Any]] + XCTAssertEqual(errors.count, 1) + } + + func testHandleCallbacksTransportFailure() { + // isSuccess = false represents a request-level (network) failure somewhere in the batch, + // as opposed to a per-token error - it should route through callRevealOnFailure instead. + let networkError = NSError(domain: "", code: -1009, userInfo: [NSLocalizedDescriptionKey: "offline"]) + + self.revealApiCallback.handleCallbacks(success: [], failure: [], isSuccess: false, errorObject: networkError) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "offline") + } + + func testOnSuccessInvalidUrl() { + self.revealApiCallback.connectionUrl = "invalid url" + self.revealApiCallback.onSuccess("token") + wait(for: [self.expectation], timeout: 20.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "unsupported URL") + } + + func testOnFailureWrapsSwiftError() { + let error = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "boom"]) + + self.revealApiCallback.onFailure(error) + wait(for: [self.expectation], timeout: 10.0) + + let errors = self.callback.data["errors"] as! [[String: NSError]] + XCTAssertEqual(errors.count, 1) + XCTAssertEqual(errors[0]["error"]?.localizedDescription, "boom") + } + + func testOnFailurePassesThroughNonErrorValues() { + let dict: [String: Any] = ["errors": [["error": "already a dict"]]] + + self.revealApiCallback.onFailure(dict) + wait(for: [self.expectation], timeout: 10.0) + + XCTAssertNotNil(self.callback.data["errors"]) + } +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index f4534f1e..3df0a7e7 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -77,4 +77,143 @@ final class skyflow_iOS_utilTests: XCTestCase { XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) XCTAssertEqual(upsertPayload["updateType"] as! String, "REPLACE") } + + // Dormant v1 PDB helpers on APIClient (untyped upsert), kept for potential future PDB reuse. + + func testGetUniqueColumn() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let upsert: [[String: Any]] = [ + ["table": "table1", "column": "email"], + ["table": "table2", "column": "ssn"] + ] + + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table1", upsert: upsert), "email") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table2", upsert: upsert), "ssn") + XCTAssertEqual(apiClient.getUniqueColumn(tableName: "table3", upsert: upsert), "") + } + + func testConstructBatchRequestBodyWithUpsert() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertEqual(postPayload[0]["upsert"] as? String, "email") + XCTAssertEqual(postPayload[0]["tableName"] as? String, "table1") + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[0]["quorum"] as? Bool, true) + } + + func testConstructBatchRequestBodyWithTokensAddsGetStep() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: true) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + // One POST insert entry, plus one GET tokenization entry appended after it. + XCTAssertEqual(postPayload.count, 2) + XCTAssertEqual(postPayload[0]["method"] as? String, "POST") + XCTAssertEqual(postPayload[1]["method"] as? String, "GET") + XCTAssertEqual(postPayload[1]["tokenization"] as? Bool, true) + XCTAssertEqual(postPayload[1]["ID"] as? String, "$responses.0.records.0.skyflow_id") + } + + func testConstructBatchRequestBodyNoUpsertNoTokens() { + let apiClient = APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()) + let records: [String: Any] = ["records": [["table": "table1", "fields": ["email": "a@b.com"]]]] + let options = ICOptions(tokens: false) + + let result = apiClient.constructBatchRequestBody(records: records, options: options) + let postPayload = result["records"] as! [[String: Any]] + + XCTAssertEqual(postPayload.count, 1) + XCTAssertNil(postPayload[0]["upsert"]) + } + + // Dormant v1 ICOptions.validateUpsert() (untyped upsert dicts), kept for potential future + // PDB reuse. Distinct from FlowVaultICOptions.validateUpsert(), which covers the live path. + + func testICOptionsValidateUpsertValid() { + let callback = DemoAPICallback(expectation: XCTestExpectation()) + let upsert: [[String: Any]] = [["table": "table1", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertFalse(options.validateUpsert()) + } + + func testICOptionsValidateUpsertEmptyArray() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let options = ICOptions(tokens: false, upsert: [], callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.UPSERT_OPTION_CANNOT_BE_EMPTY().getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingTableKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_TABLE_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertMissingColumnKey() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.MISSING_COLUMN_NAME_IN_USERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyTableValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "", "column": "email"]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testICOptionsValidateUpsertEmptyColumnValue() { + let expectation = XCTestExpectation() + let callback = DemoAPICallback(expectation: expectation) + let upsert: [[String: Any]] = [["table": "table1", "column": ""]] + let options = ICOptions(tokens: false, upsert: upsert, callback: callback, contextOptions: ContextOptions()) + + XCTAssertTrue(options.validateUpsert()) + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(callback.receivedResponse, ErrorCodes.COLUMN_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions()).localizedDescription) + } + + func testFlowVaultUpdateRequestBody() { + let records: [[String: Any]] = [ + ["skyflowID": "id1", "tableName": "table1", "data": ["email": "a@b.com"]], + ["skyflowID": "id2", "tableName": "table2", "data": ["name": "chiku"]] + ] + + let result = FlowVaultUpdateRequestBody.createRequestBody(vaultID: "vault123", records: records) + + XCTAssertEqual(result["vaultID"] as? String, "vault123") + let resultRecords = result["records"] as! [[String: Any]] + XCTAssertEqual(resultRecords.count, 2) + XCTAssertEqual(resultRecords[0]["skyflowID"] as? String, "id1") + XCTAssertEqual(resultRecords[0]["tableName"] as? String, "table1") + XCTAssertEqual(resultRecords[1]["tableName"] as? String, "table2") + } } From e83abe3d13c33e491277c8788513ddf3e1a38cba Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Mon, 27 Jul 2026 12:22:16 +0530 Subject: [PATCH 06/14] SK-3005 request and response structure change for flowvaults --- .../collect/FlowVaultCollectAPICallback.swift | 46 ++++++------ .../collect/FlowVaultInsertAPICallback.swift | 73 +++++++++++-------- .../reveal/FlowVaultRevealApiCallback.swift | 18 ++--- .../Skyflow/reveal/RevealValueCallback.swift | 14 ++-- .../skyflow_iOS_collectUtilTests.swift | 69 +++++++----------- .../skyflow_iOS_insertUtilTests 2.swift | 53 +++++++------- .../skyflow_iOS_revealUtilTests.swift | 41 ++++------- 7 files changed, 146 insertions(+), 168 deletions(-) diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift index 3eae2ca3..53065dcb 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -49,12 +49,7 @@ internal class FlowVaultCollectAPICallback: Callback { self.callback.onFailure(response) return } - let errors = response["errors"] as? [[String: Any]] ?? [] - if errors.isEmpty { - self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) - } else { - self.callback.onFailure(response) - } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) } catch { self.callback.onFailure(error) } @@ -88,7 +83,6 @@ internal class FlowVaultCollectAPICallback: Callback { mergedErrors.append(response) } else { mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) } } catch { mergedErrors.append(["error": error.localizedDescription]) @@ -114,7 +108,6 @@ internal class FlowVaultCollectAPICallback: Callback { mergedErrors.append(response) } else { mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) } } catch { mergedErrors.append(["error": error.localizedDescription]) @@ -234,22 +227,31 @@ internal class FlowVaultCollectAPICallback: Callback { var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) if let safeData = data { guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { - return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "code": httpResponse.statusCode]] + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] } - if let errorDetails = errorResponse["error"] as? [String: Any], - let message = errorDetails["message"] as? String { - description = message + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description + } + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] } if let requestId = httpResponse.allHeaderFields["x-request-id"] { description += " - request-id: \(requestId)" } } - return ["error": ["message": description, "code": httpResponse.statusCode]] + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] } } guard let safeData = data else { - return ["records": [], "errors": []] + return ["records": []] } return try getCollectResponseBody(data: safeData) @@ -258,8 +260,7 @@ internal class FlowVaultCollectAPICallback: Callback { func getCollectResponseBody(data: Data) throws -> [String: Any]{ let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] - var successRecords: [[String: Any]] = [] - var errorRecords: [[String: Any]] = [] + var records: [[String: Any]] = [] let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] for entry in responseRecords { @@ -268,23 +269,24 @@ internal class FlowVaultCollectAPICallback: Callback { if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } - errorRecords.append(errorEntry) + records.append(errorEntry) } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } var fields: [String: Any] = [:] - if let skyflowID = entry["skyflowID"] { fields["skyflow_id"] = skyflowID } if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { fields[column] = tokenValue } } - var successEntry: [String: Any] = ["fields": fields] - if let tableName = entry["tableName"] { successEntry["table"] = tableName } + successEntry["fields"] = fields if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } - successRecords.append(successEntry) + records.append(successEntry) } } - return ["records": successRecords, "errors": errorRecords] + return ["records": records] } } diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift index 633b45ef..6e360162 100644 --- a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -44,12 +44,11 @@ internal class FlowVaultInsertAPICallback: Callback { let task = session.dataTask(with: request) { data, response, error in do { let response = try self.processResponse(data: data, response: response, error: error) - let errors = response["errors"] as? [[String: Any]] ?? [] - if errors.isEmpty { - self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) - } else { + if response["error"] != nil { self.callback.onFailure(response) + return } + self.callback.onSuccess(["records": response["records"] as? [[String: Any]] ?? []]) } catch { self.callback.onFailure(error) } @@ -79,8 +78,11 @@ internal class FlowVaultInsertAPICallback: Callback { defer { group.leave() } do { let response = try self.processResponse(data: data, response: response, error: error) - mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } } catch { mergedErrors.append(["error": error.localizedDescription]) } @@ -101,8 +103,11 @@ internal class FlowVaultInsertAPICallback: Callback { defer { group.leave() } do { let response = try self.processResponse(data: data, response: response, error: error) - mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) - mergedErrors.append(contentsOf: response["errors"] as? [[String: Any]] ?? []) + if response["error"] != nil { + mergedErrors.append(response) + } else { + mergedRecords.append(contentsOf: response["records"] as? [[String: Any]] ?? []) + } } catch { mergedErrors.append(["error": error.localizedDescription]) } @@ -204,7 +209,7 @@ internal class FlowVaultInsertAPICallback: Callback { func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { if error != nil || response == nil { - throw error! + return ["error": ["message": (error)?.localizedDescription ?? "Unknown error"]] } if let httpResponse = response as? HTTPURLResponse { @@ -218,28 +223,34 @@ internal class FlowVaultInsertAPICallback: Callback { jsonObject["records"] != nil { return try getCollectResponseBody(data: safeData) } - var description = "Insert call failed with the following status code" + String(httpResponse.statusCode) - var errorObject: Error = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) - + var description = "Insert call failed with the following status code " + String(httpResponse.statusCode) if let safeData = data { - if let desc = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any], - let errorDict = desc["error"] as? [String: Any], - let message = errorDict["message"] as? String { - description = message - if let requestId = httpResponse.allHeaderFields["x-request-id"] { - description += " - request-id: \(requestId)" + guard let errorResponse = try? JSONSerialization.jsonObject(with: safeData, options: .allowFragments) as? [String: Any] else { + return ["error": ["message": String(data: safeData, encoding: .utf8) ?? "Unknown error", "httpCode": httpResponse.statusCode]] + } + // Pass the vault's structured error object (grpcCode/httpStatus/details) through + // as-is, only patching in the request-id and defaulting httpCode when absent. + if var errorDict = errorResponse["error"] as? [String: Any] { + if let message = errorDict["message"] as? String { + description = message + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" + } + errorDict["message"] = description } - errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: description).getErrorObject(contextOptions: self.contextOptions) - } else { - errorObject = ErrorCodes.APIError(code: httpResponse.statusCode, message: String(data: safeData, encoding: .utf8) ?? "Unknown error").getErrorObject(contextOptions: self.contextOptions) + if errorDict["httpCode"] == nil { errorDict["httpCode"] = httpResponse.statusCode } + return ["error": errorDict] + } + if let requestId = httpResponse.allHeaderFields["x-request-id"] { + description += " - request-id: \(requestId)" } } - throw errorObject + return ["error": ["message": description, "httpCode": httpResponse.statusCode]] } } guard let safeData = data else { - return ["records": [], "errors": []] + return ["records": []] } return try getCollectResponseBody(data: safeData) @@ -248,8 +259,7 @@ internal class FlowVaultInsertAPICallback: Callback { func getCollectResponseBody(data: Data) throws -> [String: Any]{ let jsonData = (try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]) ?? [:] - var successRecords: [[String: Any]] = [] - var errorRecords: [[String: Any]] = [] + var records: [[String: Any]] = [] let responseRecords = jsonData["records"] as? [[String: Any]] ?? [] for entry in responseRecords { @@ -258,23 +268,24 @@ internal class FlowVaultInsertAPICallback: Callback { if let skyflowID = entry["skyflowID"] { errorEntry["skyflowID"] = skyflowID } if let tableName = entry["tableName"] { errorEntry["tableName"] = tableName } if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } - errorRecords.append(errorEntry) + records.append(errorEntry) } else { + var successEntry: [String: Any] = [:] + if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } + if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } var fields: [String: Any] = [:] - if let skyflowID = entry["skyflowID"] { fields["skyflow_id"] = skyflowID } if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { fields[column] = tokenValue } } - var successEntry: [String: Any] = ["fields": fields] - if let tableName = entry["tableName"] { successEntry["table"] = tableName } + successEntry["fields"] = fields if let hashedData = entry["hashedData"] as? [String: Any] { successEntry["hashedData"] = self.buildFieldsDict(dict: hashedData) } if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } - successRecords.append(successEntry) + records.append(successEntry) } } - return ["records": successRecords, "errors": errorRecords] + return ["records": records] } } diff --git a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift index 164343f6..7932295e 100644 --- a/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift +++ b/Sources/Skyflow/reveal/FlowVaultRevealApiCallback.swift @@ -37,12 +37,7 @@ class FlowVaultRevealAPICallback: Callback { let task = session.dataTask(with: request) { data, response, error in do { let response = try self.processResponse(data: data, response: response, error: error) - let errors = response["errors"] as? [[String: Any]] ?? [] - if errors.isEmpty { - self.callback.onSuccess(response) - } else { - self.callback.onFailure(response) - } + self.callback.onSuccess(response) } catch { self.callRevealOnFailure(callback: self.callback, errorObject: error) } @@ -129,7 +124,7 @@ class FlowVaultRevealAPICallback: Callback { } guard let safeData = data else { - return ["records": [], "errors": []] + return ["records": []] } return try getDetokenizeResponseBody(data: safeData) @@ -137,8 +132,7 @@ class FlowVaultRevealAPICallback: Callback { func getDetokenizeResponseBody(data: Data) throws -> [String: Any] { let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any] - var successRecords: [[String: Any]] = [] - var errorRecords: [[String: Any]] = [] + var records: [[String: Any]] = [] let responseRecords = jsonData["response"] as? [[String: Any]] ?? [] for entry in responseRecords { @@ -146,7 +140,7 @@ class FlowVaultRevealAPICallback: Callback { var errorEntry: [String: Any] = ["error": error] if let token = entry["token"] { errorEntry["token"] = token } if let httpCode = entry["httpCode"] { errorEntry["httpCode"] = httpCode } - errorRecords.append(errorEntry) + records.append(errorEntry) } else { var successEntry: [String: Any] = [:] if let token = entry["token"] { successEntry["token"] = token } @@ -154,10 +148,10 @@ class FlowVaultRevealAPICallback: Callback { if let tokenGroupName = entry["tokenGroupName"] { successEntry["tokenGroupName"] = tokenGroupName } if let httpCode = entry["httpCode"] { successEntry["httpCode"] = httpCode } if let metadata = entry["metadata"] as? [String: Any] { successEntry["metadata"] = metadata } - successRecords.append(successEntry) + records.append(successEntry) } } - return ["records": successRecords, "errors": errorRecords] + return ["records": records] } } diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Sources/Skyflow/reveal/RevealValueCallback.swift index ac9803ec..50407c2f 100644 --- a/Sources/Skyflow/reveal/RevealValueCallback.swift +++ b/Sources/Skyflow/reveal/RevealValueCallback.swift @@ -21,10 +21,18 @@ internal class RevealValueCallback: Callback { let responseJson = responseBody as? [String: Any] ?? [:] var response: [String: Any] = [:] var successResponses: [[String: Any]] = [] + var errors: [[String: Any]] = [] + // Success and per-token failure entries now arrive together in one "records" array, + // each distinguished by the presence of an "error" key. if let records = responseJson["records"] as? [Any] { for record in records { - guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } + guard let dict = record as? [String: Any] else { continue } + if dict["error"] != nil { + errors.append(dict) + continue + } + guard let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token @@ -35,10 +43,6 @@ internal class RevealValueCallback: Callback { if successResponses.count != 0 { response["success"] = successResponses } - var errors = [] as [[String: Any]] - if let responseErrors = responseJson["errors"] as? [[String: Any]] { - errors = responseErrors - } let tokensToErrors = getTokensToErrors(errors) if errors.count != 0 { response["errors"] = errors diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 3115045f..b36d351f 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -65,14 +65,12 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - let errors = result["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as! String, "table") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") XCTAssertNil(fields["tokens"]) - XCTAssertTrue(errors.isEmpty) } catch { XCTFail(error.localizedDescription) } @@ -88,9 +86,9 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as! String, "table") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") let fieldTokens = fields["field"] as! [[String: Any]] XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { @@ -132,9 +130,8 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) let records = processedData["records"] as! [[String: Any]] - XCTAssertEqual(records[0]["table"] as! String, "table") - let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") } catch { XCTFail(error.localizedDescription) @@ -162,7 +159,6 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: arrayData) XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) - XCTAssertEqual((result["errors"] as? [[String: Any]])?.count, 0) } catch { XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } @@ -262,14 +258,12 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { do { let processedInsert = try collectCallback.processResponse(data: insertResponseData, response: insertUrlResponse, error: nil) let insertRecords = processedInsert["records"] as! [[String: Any]] - let errors = processedInsert["errors"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - XCTAssertEqual(insertRecords[0]["table"] as? String, "table") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") let fields = insertRecords[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as? String, "SID") XCTAssertEqual(fields["field"] as? String, "value") - XCTAssertTrue(errors.isEmpty) } catch { XCTFail("Insert scenario failed: \(error)") } @@ -302,12 +296,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { do { let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertEqual(records.count, 1) - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as? String, "insert failed") - XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + XCTAssertEqual(records.count, 2) + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["error"] as? String, "insert failed") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { XCTFail("Partial insert failure scenario failed: \(error)") } @@ -345,12 +338,10 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertTrue(records.isEmpty) - XCTAssertEqual(errors.count, 2) - XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") - XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) } catch { XCTFail("Full failure scenario should not throw: \(error)") } @@ -386,13 +377,10 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { do { let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertEqual(records.count, 1) - let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { XCTFail("Partial (207) scenario should not throw: \(error)") } @@ -425,13 +413,11 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertTrue(errors.isEmpty) XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as? String, "table1") + XCTAssertEqual(records[0]["tableName"] as? String, "table1") + XCTAssertEqual(records[0]["skyflowID"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as? String, "f30c8ccf-7e86-46b4-be74-0b2db44e4b87") XCTAssertNil(fields["data"]) let nameTokens = fields["name"] as! [[String: Any]] XCTAssertEqual(nameTokens[0]["token"] as? String, "sagwm") @@ -464,11 +450,9 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertTrue(records.isEmpty) - XCTAssertEqual(errors.count, 2) - XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") } catch { XCTFail("Update full failure response should not throw: \(error)") } @@ -505,13 +489,10 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertEqual(records.count, 1) - let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["skyflowID"] as? String, "b187b5b6-28b4-4881-93fa-4ef10e30b20e") + XCTAssertEqual(records[1]["httpCode"] as? Int, 400) } catch { XCTFail("Update partial response should not throw: \(error)") } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index fb828838..1750ddcd 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -34,8 +34,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { self.collectCallback.onSuccess("string") wait(for: [expectation], timeout: 20.0) - let result = callback.receivedResponse - XCTAssert(result.contains("unsupported URL")) + let errorObject = callback.data["error"] as! [String: Any] + let msg = errorObject["message"] as! String + XCTAssert(msg.contains("unsupported URL")) } func testGetRequestSession() { @@ -64,13 +65,10 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: data) let records = result["records"] as! [[String: Any]] - let errors = result["errors"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as! String, "table") - let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") - XCTAssertTrue(errors.isEmpty) + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") } catch { XCTFail(error.localizedDescription) } @@ -86,9 +84,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let records = result["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) - XCTAssertEqual(records[0]["table"] as! String, "table") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") let fieldTokens = fields["field"] as! [[String: Any]] XCTAssertEqual(fieldTokens[0]["token"] as? String, "tok") } catch { @@ -130,9 +128,8 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let processedData = try self.collectCallback.processResponse(data: data, response: response, error: nil) let records = processedData["records"] as! [[String: Any]] - XCTAssertEqual(records[0]["table"] as! String, "table") - let fields = records[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as! String, "SID") + XCTAssertEqual(records[0]["tableName"] as! String, "table") + XCTAssertEqual(records[0]["skyflowID"] as! String, "SID") } catch { XCTFail(error.localizedDescription) @@ -140,14 +137,16 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { } func testProcessResponseError() { - // A genuine connection/network-level error (URLSession error, no HTTP response reached). + // A genuine connection/network-level error (URLSession error, no HTTP response reached) + // should not throw - it returns a top-level {"error": {...}} dict directly. let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) do { - _ = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) - XCTFail("Should have thrown error") + let processedData = try self.collectCallback.processResponse(data: nil, response: nil, error: networkError) + let errorDict = processedData["error"] as! [String: Any] + XCTAssertEqual(errorDict["message"] as? String, "The Internet connection appears to be offline.") } catch { - XCTAssertEqual((error as NSError).code, -1009) + XCTFail("Should not throw for a connection-level error: \(error)") } } @@ -158,7 +157,6 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let arrayData = try JSONSerialization.data(withJSONObject: ["not", "an", "object"], options: .fragmentsAllowed) let result = try self.collectCallback.getCollectResponseBody(data: arrayData) XCTAssertEqual((result["records"] as? [[String: Any]])?.count, 0) - XCTAssertEqual((result["errors"] as? [[String: Any]])?.count, 0) } catch { XCTFail("Malformed top-level JSON should not throw or crash: \(error)") } @@ -170,11 +168,11 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let data = try JSONSerialization.data(withJSONObject: response, options: .fragmentsAllowed) let response = HTTPURLResponse(url: URL(string: "https://example.org")!, statusCode: 500, httpVersion: "1.1", headerFields: ["x-request-id": "RID"]) - _ = try self.collectCallback.processResponse(data: data, response: response, error: nil) - XCTFail("Not throwing on Api Error") - + let res = try self.collectCallback.processResponse(data: data, response: response, error: nil) + let message = (res["error"] as! [String: Any])["message"] as! String + XCTAssertEqual(message, "Internal Server Error - request-id: RID") } catch { - XCTAssertEqual(error.localizedDescription, "Internal Server Error - request-id: RID") + XCTFail("Should not throw on Api Error: \(error)") } } @@ -255,9 +253,9 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { let insertRecords = processedInsert["records"] as! [[String: Any]] XCTAssertEqual(insertRecords.count, 1) - XCTAssertEqual(insertRecords[0]["table"] as? String, "table") + XCTAssertEqual(insertRecords[0]["tableName"] as? String, "table") + XCTAssertEqual(insertRecords[0]["skyflowID"] as? String, "SID") let fields = insertRecords[0]["fields"] as! [String: Any] - XCTAssertEqual(fields["skyflow_id"] as? String, "SID") XCTAssertEqual(fields["field"] as? String, "value") } catch { XCTFail("Insert scenario failed: \(error)") @@ -291,13 +289,12 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { do { let processed = try collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) + XCTAssertNil(processed["error"], "Should not collapse into a generic top-level error when the body has a records array") let records = processed["records"] as! [[String: Any]] - let errors = processed["errors"] as! [[String: Any]] - XCTAssertTrue(records.isEmpty) - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") - XCTAssertEqual(errors[0]["httpCode"] as? Int, 400) + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["error"] as? String, "Invalid request. Table name table not present for record. Specify a valid table name.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 400) } catch { XCTFail("Full failure scenario should not throw: \(error)") } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index fefa739a..eabbdaa2 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -114,11 +114,10 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) let records = response["records"] as! [[String: Any]] - let errors = response["errors"] as! [[String: Any]] - XCTAssertTrue(errors.isEmpty) XCTAssertEqual(records.count, 1) XCTAssertEqual(records[0]["token"] as? String, "token") XCTAssertEqual(records[0]["value"] as? String, "value") + XCTAssertNil(records[0]["error"]) } catch { XCTFail(error.localizedDescription) } @@ -154,12 +153,11 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) let records = response["records"] as! [[String: Any]] - let errors = response["errors"] as! [[String: Any]] - XCTAssertEqual(records.count, 1) + XCTAssertEqual(records.count, 2) XCTAssertEqual(records[0]["token"] as? String, "token1") - XCTAssertEqual(errors.count, 1) - XCTAssertEqual(errors[0]["token"] as? String, "token2") - XCTAssertEqual(errors[0]["error"] as? String, "Invalid Token") + XCTAssertNil(records[0]["error"]) + XCTAssertEqual(records[1]["token"] as? String, "token2") + XCTAssertEqual(records[1]["error"] as? String, "Invalid Token") } catch { XCTFail(error.localizedDescription) } @@ -179,12 +177,10 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let responseData = try JSONSerialization.data(withJSONObject: revealedResponse, options: .fragmentsAllowed) let response = try self.revealApiCallback.processResponse(data: responseData, response: httpResponse, error: nil) let records = response["records"] as! [[String: Any]] - let errors = response["errors"] as! [[String: Any]] - XCTAssertTrue(records.isEmpty) - XCTAssertEqual(errors.count, 2) - XCTAssertEqual(errors[0]["error"] as? String, "Detokenize failed. Token dedwimm is invalid. Specify a valid token.") - XCTAssertEqual(errors[0]["httpCode"] as? Int, 404) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["error"] as? String, "Detokenize failed. Token dedwimm is invalid. Specify a valid token.") + XCTAssertEqual(records[0]["httpCode"] as? Int, 404) } catch { XCTFail("Full failure with 404 outer status should not throw: \(error)") } @@ -409,8 +405,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "John"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "John"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions()) @@ -440,8 +435,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "4567890"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "4567890"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-X", translation: ["X": "[0-9]"])) @@ -473,8 +467,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX-XXX", translation: ["X": "[0-9]"])) @@ -506,8 +499,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "XXX-XXX", translation: ["X": "[0-9]"])) @@ -540,8 +532,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "12345678"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "12345678"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -573,8 +564,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["X": "[0-9]"])) @@ -606,8 +596,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let successToken = "123" let failureToken = "1234" let response = [ - "records": [["token": successToken, "value": "name"]], - "errors": [["token": failureToken, "error": "Invalid Token"]] + "records": [["token": successToken, "value": "name"], ["token": failureToken, "error": "Invalid Token"]] ] let successElement = self.container.create(input: RevealElementInput(token: successToken, label: "name"), options: RevealElementOptions(format: "+91 XXX-XXX", translation: ["Y": "[0-9]"])) From 7534467693369f81b35627aee7843260c14d8b53 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 28 Jul 2026 08:37:21 +0530 Subject: [PATCH 07/14] SK-3005 request and response structure change for flowvaults --- .../Skyflow/collect/CollectContainer.swift | 2 +- Sources/Skyflow/collect/CollectResponse.swift | 103 ++++++++++++++++ Sources/Skyflow/core/SkyflowAPIError.swift | 37 ++++++ Sources/Skyflow/errors/ErrorCodes.swift | 12 +- Sources/Skyflow/reveal/RevealContainer.swift | 2 +- Sources/Skyflow/reveal/RevealResponse.swift | 110 ++++++++++++++++++ .../Skyflow/reveal/RevealValueCallback.swift | 40 +++---- Tests/skyflow-iOS-collectTests/DemoImpl.swift | 23 ++++ .../skyflow_iOS_collectTests.swift | 42 +++---- Tests/skyflow-iOS-errorTests/DemoImpl.swift | 19 ++- .../Skyflow_iOS_collectErrorTests.swift | 6 +- .../Skyflow_iOS_revealErrorTests.swift | 6 +- Tests/skyflow-iOS-revealTests/DemoImpl.swift | 23 ++++ .../skyflow_iOS_revealTests.swift | 2 +- .../skyflow-iOS-scenarioTests/DemoImpl.swift | 4 - Tests/skyflow-iOS-utilTests/DemoImpl.swift | 19 ++- .../skyflow_iOS_collectUtilTests.swift | 2 +- .../skyflow_iOS_insertUtilTests 2.swift | 2 +- .../skyflow_iOS_revealUtilTests.swift | 57 +++++---- 19 files changed, 413 insertions(+), 98 deletions(-) create mode 100644 Sources/Skyflow/collect/CollectResponse.swift create mode 100644 Sources/Skyflow/core/SkyflowAPIError.swift create mode 100644 Sources/Skyflow/reveal/RevealResponse.swift diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainer.swift index 07ac2707..0f8c8b4c 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainer.swift @@ -22,7 +22,7 @@ public extension Container { return skyflowElement } - func collect(callback: Callback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { + func collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) where T: CollectContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .COLLECT_CONTAINER if self.skyflow.vaultID.isEmpty { diff --git a/Sources/Skyflow/collect/CollectResponse.swift b/Sources/Skyflow/collect/CollectResponse.swift new file mode 100644 index 00000000..b2227c44 --- /dev/null +++ b/Sources/Skyflow/collect/CollectResponse.swift @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +import Foundation + +public struct CollectResponse { + public let records: [CollectRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { CollectRecord($0) } + } +} + +// Each entry in "records" is either a successfully inserted/updated CollectRecordSuccess or a +// CollectRecordError describing why that entry failed - the vault returns both together in the same +// array, each tagged with its own httpCode. +public enum CollectRecord { + case success(CollectRecordSuccess) + case failure(CollectRecordError) + + init(_ dict: [String: Any]) { + if let error = dict["error"] as? String { + self = .failure(CollectRecordError(dict, error: error)) + } else { + self = .success(CollectRecordSuccess(dict)) + } + } + + public var record: CollectRecordSuccess? { + if case .success(let record) = self { return record } + return nil + } + + public var error: CollectRecordError? { + if case .failure(let error) = self { return error } + return nil + } +} + +public struct CollectRecordSuccess { + public let tableName: String? + public let skyflowID: String? + // Keyed by column name, e.g. "card_number": [{"token": "...", "tokenGroupName": "..."}]. + public let fields: [String: Any]? + // Keyed by column name, e.g. "card_number": [{"data": "...", "hashName": "..."}]. + public let hashedData: [String: Any]? + public let httpCode: Int? + + init(_ dict: [String: Any]) { + self.tableName = dict["tableName"] as? String + self.skyflowID = dict["skyflowID"] as? String + self.fields = dict["fields"] as? [String: Any] + self.hashedData = dict["hashedData"] as? [String: Any] + self.httpCode = dict["httpCode"] as? Int + } +} + +public struct CollectRecordError { + public let error: String + public let skyflowID: String? + public let tableName: String? + public let httpCode: Int? + + init(_ dict: [String: Any], error: String) { + self.error = error + self.skyflowID = dict["skyflowID"] as? String + self.tableName = dict["tableName"] as? String + self.httpCode = dict["httpCode"] as? Int + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into CollectResponse +// for you. Use it directly with the original callback-based collect(callback:options:). +public class CollectCallback: Callback { + private let successHandler: (CollectResponse) -> Void + private let failureHandler: (Any) -> Void + + public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (Any) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = CollectResponse(responseBody) else { + failureHandler(responseBody) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-record failure inside CollectResponse.records. + public func onFailure(_ error: Any) { + guard let apiError = SkyflowAPIError(error) else { + failureHandler(error) + return + } + failureHandler(apiError) + } +} diff --git a/Sources/Skyflow/core/SkyflowAPIError.swift b/Sources/Skyflow/core/SkyflowAPIError.swift new file mode 100644 index 00000000..38619cea --- /dev/null +++ b/Sources/Skyflow/core/SkyflowAPIError.swift @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Shared structured-error shape delivered via Callback.onFailure when an entire Collect or +// Reveal request fails (e.g. vault not found, network error) rather than a per-record/per-token +// failure inside the corresponding records array. + +import Foundation + +public struct SkyflowAPIError { + // Shadows Swift's built-in Error protocol within this scope - use Swift.Error if ever + // needed here. + public struct Error { + public let grpcCode: Int? + public let httpCode: Int? + public let message: String? + public let httpStatus: String? + public let details: [Any]? + + init(_ dict: [String: Any]) { + self.grpcCode = dict["grpcCode"] as? Int + self.httpCode = dict["httpCode"] as? Int + self.message = dict["message"] as? String + self.httpStatus = dict["httpStatus"] as? String + self.details = dict["details"] as? [Any] + } + } + + public let error: Error + + public init?(_ error: Any) { + guard let dict = error as? [String: Any], + let detail = dict["error"] as? [String: Any] else { return nil } + self.error = Error(detail) + } +} diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/Skyflow/errors/ErrorCodes.swift index 67bb66e1..e1a34a2d 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/Skyflow/errors/ErrorCodes.swift @@ -133,14 +133,4 @@ internal enum ErrorCodes: CustomStringConvertible { } } -public class SkyflowError: NSError { - var xml: String = "" - - func setXML(xml: String) { - self.xml = xml - } - - public func getXML() -> String { - return self.xml - } -} +public class SkyflowError: NSError {} diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index c7254c25..71810ab6 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -25,7 +25,7 @@ public extension Container { return revealElement } - func reveal(callback: Callback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { + func reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) where T: RevealContainer { var tempContextOptions = self.skyflow.contextOptions tempContextOptions.interface = .REVEAL_CONTAINER if self.skyflow.vaultID.isEmpty { diff --git a/Sources/Skyflow/reveal/RevealResponse.swift b/Sources/Skyflow/reveal/RevealResponse.swift new file mode 100644 index 00000000..7941b016 --- /dev/null +++ b/Sources/Skyflow/reveal/RevealResponse.swift @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Public typed wrappers around the dictionary delivered via Callback.onSuccess for both +// Client.detokenize() and Container.reveal() - both deliver the same merged +// {"records": [...]} shape (success and per-token failures share one array, matching Collect's +// convention). The Callback protocol still delivers raw [String: Any] as before - these types +// don't change that, they just wrap it directly (no JSONDecoder round-trip). Usage: +// +// public func onSuccess(_ responseBody: Any) { +// guard let response = Skyflow.RevealResponse(responseBody) else { return } +// ... +// } + +import Foundation + +public struct RevealResponse { + public let records: [RevealRecord] + + public init?(_ responseBody: Any) { + guard let dict = responseBody as? [String: Any], + let recordDicts = dict["records"] as? [[String: Any]] else { return nil } + self.records = recordDicts.map { RevealRecord($0) } + } +} + +// Each entry in "records" is either a successfully revealed RevealRecordSuccess or a +// RevealRecordError describing why that token failed - the vault returns both together in the +// same array, each tagged with its own httpCode. +public enum RevealRecord { + case success(RevealRecordSuccess) + case failure(RevealRecordError) + + init(_ dict: [String: Any]) { + if let error = dict["error"] as? String { + self = .failure(RevealRecordError(dict, error: error)) + } else { + self = .success(RevealRecordSuccess(dict)) + } + } + + public var record: RevealRecordSuccess? { + if case .success(let record) = self { return record } + return nil + } + + public var error: RevealRecordError? { + if case .failure(let error) = self { return error } + return nil + } +} + +public struct RevealRecordSuccess { + public let token: String? + public let tokenGroupName: String? + // Keyed by whatever fields the vault includes (e.g. "skyflowID", "tableName"). + public let metadata: [String: Any]? + public let httpCode: Int? + + init(_ dict: [String: Any]) { + self.token = dict["token"] as? String + self.tokenGroupName = dict["tokenGroupName"] as? String + self.metadata = dict["metadata"] as? [String: Any] + self.httpCode = dict["httpCode"] as? Int + } +} + +public struct RevealRecordError { + public let error: String + public let token: String? + public let httpCode: Int? + + init(_ dict: [String: Any], error: String) { + self.error = error + self.token = dict["token"] as? String + self.httpCode = dict["httpCode"] as? Int + } +} + +// A ready-made Skyflow.Callback that unwraps the raw responseBody/error into RevealResponse +// for you. Use it directly with the callback-based reveal(callback:options:) - RevealValueCallback +// merges success/failure into one "records" array (matching Collect's convention), so the same +// RevealResponse type used for Client.detokenize() applies here too. +public class RevealCallback: Callback { + private let successHandler: (RevealResponse) -> Void + private let failureHandler: (Any) -> Void + + public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (Any) -> Void) { + self.successHandler = onSuccess + self.failureHandler = onFailure + } + + public func onSuccess(_ responseBody: Any) { + guard let response = RevealResponse(responseBody) else { + failureHandler(responseBody) + return + } + successHandler(response) + } + + // Delivered when the entire request fails (e.g. vault not found, network error) rather + // than a per-token failure inside RevealResponse.records, as a raw dictionary matching + // SkyflowAPIError's shape - construct one yourself via SkyflowAPIError(error) if needed. + // Note: RevealContainer.reveal()'s validation errors (empty vaultID, unmounted element, + // etc.) are raw NSError, not this shape. + public func onFailure(_ error: Any) { + failureHandler(error) + } +} diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Sources/Skyflow/reveal/RevealValueCallback.swift index 50407c2f..b105af78 100644 --- a/Sources/Skyflow/reveal/RevealValueCallback.swift +++ b/Sources/Skyflow/reveal/RevealValueCallback.swift @@ -20,33 +20,29 @@ internal class RevealValueCallback: Callback { let responseJson = responseBody as? [String: Any] ?? [:] var response: [String: Any] = [:] - var successResponses: [[String: Any]] = [] + var records: [[String: Any]] = [] var errors: [[String: Any]] = [] - // Success and per-token failure entries now arrive together in one "records" array, - // each distinguished by the presence of an "error" key. - if let records = responseJson["records"] as? [Any] { - for record in records { + // Success and per-token failure entries arrive together in one "records" array (matching + // Collect's convention and the JS SDK's shape), each distinguished by an "error" key. + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { guard let dict = record as? [String: Any] else { continue } if dict["error"] != nil { errors.append(dict) + records.append(dict) continue } guard let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token - successResponses.append(dict) + records.append(dict) } } - if successResponses.count != 0 { - response["success"] = successResponses - } + response["records"] = records let tokensToErrors = getTokensToErrors(errors) - if errors.count != 0 { - response["errors"] = errors - } DispatchQueue.main.async { for revealElement in self.revealElements { @@ -72,31 +68,27 @@ internal class RevealValueCallback: Callback { if let responseJson = error as? [String: Any] { var tokens: [String: String] = [:] - var successResponses: [[String: Any]] = [] + var records: [[String: Any]] = [] - if let records = responseJson["records"] as? [Any] { - for record in records { + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } let value = dict["value"] as? String tokens[token] = value ?? token - successResponses.append(dict) + records.append(dict) } } - if successResponses.count != 0 { - response["success"] = successResponses - } var errors = [] as [[String: Any]] if let responseErrors = responseJson["errors"] as? [[String: Any]] { errors = responseErrors } - + records.append(contentsOf: errors) + + response["records"] = records let tokensToErrors = getTokensToErrors(errors) - if errors.count != 0 { - response["errors"] = errors - } - + DispatchQueue.main.async { for revealElement in self.revealElements { if let v = tokens[revealElement.revealInput.token]{ diff --git a/Tests/skyflow-iOS-collectTests/DemoImpl.swift b/Tests/skyflow-iOS-collectTests/DemoImpl.swift index 7d3e3f85..e89f3a1f 100644 --- a/Tests/skyflow-iOS-collectTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-collectTests/DemoImpl.swift @@ -42,12 +42,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "default" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } do { let dataString = String(data: try JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) if let unwrapped = dataString { @@ -68,4 +80,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index d36871d0..a59050f8 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -237,7 +237,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -277,7 +277,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -314,7 +314,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -339,7 +339,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -439,7 +439,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber INVALID_CARD_NUMBER\n") @@ -462,7 +462,7 @@ final class skyflow_iOS_collectTests: XCTestCase { textField?.textFieldDidEndEditing(textField!.textField) let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber Regex match failed\n") @@ -513,7 +513,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectFailure = XCTestExpectation(description: "Should fail") let myCallback = DemoAPICallback(expectation: expectFailure) - mycontainer?.collect(callback: myCallback) + mycontainer?.collect(callback: myCallback.asCollectCallback) wait(for: [expectFailure], timeout: 10.0) XCTAssertEqual(myCallback.receivedResponse, "for cardnumber triggered error\n") @@ -725,7 +725,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_ID().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) @@ -737,7 +737,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) @@ -749,7 +749,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -762,7 +762,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -777,7 +777,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -790,7 +790,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -803,7 +803,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -816,7 +816,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -829,7 +829,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -842,7 +842,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -855,7 +855,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -866,7 +866,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:[])) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:[])) wait(for: [expectation], timeout: 20.0) @@ -878,7 +878,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) @@ -890,7 +890,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) diff --git a/Tests/skyflow-iOS-errorTests/DemoImpl.swift b/Tests/skyflow-iOS-errorTests/DemoImpl.swift index 71cb4d32..444ad3c3 100644 --- a/Tests/skyflow-iOS-errorTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-errorTests/DemoImpl.swift @@ -41,13 +41,19 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { - if responseBody is String { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + } else if responseBody is String { self.receivedResponse = responseBody as! String } else { @@ -66,4 +72,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 43df5994..5cdbf3f5 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -184,7 +184,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -219,7 +219,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) @@ -244,7 +244,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 10.0) diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift index 075157ec..74da44b9 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_revealErrorTests.swift @@ -89,7 +89,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let revealElement = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse @@ -108,7 +108,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should return reveal output")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) let result = callback.receivedResponse @@ -122,7 +122,7 @@ class Skyflow_iOS_revealErrorTests: XCTestCase { let revealContainer = clientWithEmptyURL.container(type: ContainerType.REVEAL, options: nil) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Reveal with empty vaultURL should fail")) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .REVEAL_CONTAINER)).localizedDescription) } diff --git a/Tests/skyflow-iOS-revealTests/DemoImpl.swift b/Tests/skyflow-iOS-revealTests/DemoImpl.swift index 73208cc0..1454099e 100644 --- a/Tests/skyflow-iOS-revealTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-revealTests/DemoImpl.swift @@ -35,12 +35,24 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation } public func onSuccess(_ responseBody: Any) { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + expectation.fulfill() + return + } + if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + expectation.fulfill() + return + } defer{ expectation.fulfill() } @@ -64,4 +76,15 @@ public class DemoAPICallback: Callback { } expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index b1445e4b..88d78772 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -177,7 +177,7 @@ class skyflow_iOS_revealTests: XCTestCase { window.addSubview(revealElement!) let callback = DemoAPICallback(expectation: expectFailure) - revealContainer?.reveal(callback: callback) + revealContainer?.reveal(callback: callback.asRevealCallback) wait(for: [expectFailure], timeout: 10.0) diff --git a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift index 29e985e0..b5b8c05c 100644 --- a/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-scenarioTests/DemoImpl.swift @@ -33,7 +33,6 @@ public class DemoTokenProvider: TokenProvider { public class DemoAPICallback: Callback { var receivedResponse: String = "" - var xml: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] var error: NSError? = nil @@ -54,9 +53,6 @@ public class DemoAPICallback: Callback { } else if error is [String: Any] { self.data = (error as! [String: Any]) } - if error is SkyflowError { - self.xml = (error as! SkyflowError).getXML() - } expectation.fulfill() } diff --git a/Tests/skyflow-iOS-utilTests/DemoImpl.swift b/Tests/skyflow-iOS-utilTests/DemoImpl.swift index e93c1169..f7315771 100644 --- a/Tests/skyflow-iOS-utilTests/DemoImpl.swift +++ b/Tests/skyflow-iOS-utilTests/DemoImpl.swift @@ -16,6 +16,8 @@ public class DemoAPICallback: Callback { var receivedResponse: String = "" var expectation: XCTestExpectation var data: [String: Any] = [:] + var collectResponse: Skyflow.CollectResponse? + var revealResponse: Skyflow.RevealResponse? public init(expectation: XCTestExpectation) { self.expectation = expectation @@ -23,7 +25,11 @@ public class DemoAPICallback: Callback { public func onSuccess(_ responseBody: Any) { print("success") - if let response = responseBody as? String { + if let response = responseBody as? Skyflow.CollectResponse { + self.collectResponse = response + } else if let response = responseBody as? Skyflow.RevealResponse { + self.revealResponse = response + } else if let response = responseBody as? String { self.receivedResponse = response } else { @@ -44,4 +50,15 @@ public class DemoAPICallback: Callback { expectation.fulfill() } + + // CollectContainer.collect(callback:)/RevealContainer.reveal(callback:) require the + // concrete Skyflow.CollectCallback/RevealCallback types - these wrap self so existing + // DemoAPICallback call sites only need a one-word change (.asCollectCallback/.asRevealCallback). + var asCollectCallback: Skyflow.CollectCallback { + Skyflow.CollectCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } + + var asRevealCallback: Skyflow.RevealCallback { + Skyflow.RevealCallback(onSuccess: { self.onSuccess($0) }, onFailure: { self.onFailure($0) }) + } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index b36d351f..bc3e5696 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -193,7 +193,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { UIWindow().addSubview(element!) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index 1750ddcd..9368f46f 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -186,7 +186,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { UIWindow().addSubview(element!) - container?.collect(callback: callback) + container?.collect(callback: callback.asCollectCallback) wait(for: [expectation], timeout: 20.0) XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index eabbdaa2..d81ff4d8 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -248,9 +248,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - XCTAssertNil(self.callback.data["errors"]) - let records = self.callback.data["success"] as! [[String: Any]] + let records = self.callback.data["records"] as! [[String: Any]] XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.allSatisfy { $0["error"] == nil }) XCTAssertEqual(element1.actualValue, "John") XCTAssertEqual(element2.actualValue, "Doe") XCTAssertEqual(element1.errorMessage.text, nil) @@ -277,9 +277,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - XCTAssertNil(self.callback.data["success"]) - let errors = self.callback.data["errors"] as! [[String: Any]] + let errors = self.callback.data["records"] as! [[String: Any]] XCTAssertEqual(errors.count, 2) + XCTAssertTrue(errors.allSatisfy { $0["error"] != nil }) XCTAssertEqual(element1.actualValue, nil) XCTAssertEqual(element2.actualValue, nil) XCTAssertEqual(element1.errorMessage.text, "Invalid Token") @@ -305,8 +305,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - XCTAssertNil(self.callback.data["success"]) - let errors = self.callback.data["errors"] as! [[String: Any]] + let errors = self.callback.data["records"] as! [[String: Any]] XCTAssertEqual(errors.count, 1) XCTAssertEqual((errors[0]["error"] as? NSError)?.code, -1009) // No token to match this error against, so the element shows no inline error. @@ -363,7 +362,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let records = self.callback.data["success"] as! [[String: Any]] + let records = self.callback.data["records"] as! [[String: Any]] XCTAssertEqual(records.count, 1) XCTAssertEqual(records[0]["token"] as? String, goodToken) XCTAssertEqual(goodElement.actualValue, "John") @@ -386,8 +385,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -417,8 +417,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -447,8 +448,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -479,8 +481,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -511,8 +514,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -544,8 +548,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -576,8 +581,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -608,8 +614,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["errors"] as! [[String: String]] - let records = self.callback.data["success"] as! [[String: String]] + let allRecords = self.callback.data["records"] as! [[String: String]] + let errors = allRecords.filter { $0["error"] != nil } + let records = allRecords.filter { $0["error"] == nil } XCTAssertEqual(errors.count, 1) XCTAssertEqual(errors[0]["token"], failureToken) @@ -636,11 +643,11 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { UIWindow().addSubview(element!) - container?.reveal(callback: callback) + container?.reveal(callback: callback.asRevealCallback) wait(for: [expectation], timeout: 20.0) print(callback.data) - let errors = callback.data["errors"] as! [[String: NSError]] + let errors = callback.data["records"] as! [[String: NSError]] XCTAssertTrue(errors[0]["error"]!.localizedDescription.contains("Token generated from 'getBearerToken' callback function is invalid")) } From 06bb4b4851c6b65e476e9abc0f72c5ab1dbdedf7 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 28 Jul 2026 09:41:52 +0530 Subject: [PATCH 08/14] SK-3005 update readme and sample --- README.md | 628 ++++++----------- .../CollectAndRevealViewController.swift | 59 +- .../ExampleAPICallback.swift | 47 +- .../ResponseStructs.swift | 22 +- Samples/CollectAndRevealSample/Podfile | 3 +- .../ExampleAPICallback.swift | 47 +- .../ComposableElements/ResponseStructs.swift | 22 +- .../ComposableElements/ViewController.swift | 67 +- Samples/ComposableElements/Podfile | 3 +- .../GetSample.xcodeproj/project.pbxproj | 640 ------------------ .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/swiftpm/Package.resolved | 23 - Samples/GetSample/GetSample/AppDelegate.swift | 36 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../GetSample/Assets.xcassets/Contents.json | 6 - .../Base.lproj/LaunchScreen.storyboard | 25 - .../GetSample/Base.lproj/Main.storyboard | 24 - .../GetSample/ExampleAPICallback.swift | 50 -- .../GetSample/ExampleTokenProvider.swift | 31 - Samples/GetSample/GetSample/Info.plist | 25 - .../GetSample/GetSample/ResponseStructs.swift | 23 - .../GetSample/GetSample/SceneDelegate.swift | 52 -- .../GetSample/GetSample/ViewController.swift | 112 --- .../GetSampleTests/GetSampleTests.swift | 36 - .../GetSampleUITests/GetSampleUITests.swift | 41 -- .../GetSampleUITestsLaunchTests.swift | 32 - Samples/GetSample/Podfile | 12 - .../InputFormatting/ExampleAPICallback.swift | 49 +- .../InputFormatting/ResponseStructs.swift | 28 +- .../InputFormatting/ViewController.swift | 44 +- Samples/InputFormatting/Podfile | 3 +- Samples/InputFormatting/Podfile.lock | 4 - Samples/UpdateDataUsingElements/Podfile | 9 +- .../ExampleAPICallback.swift | 47 +- .../ResponseStructs.swift | 23 +- .../ViewController.swift | 11 +- .../xcshareddata/swiftpm/Package.resolved | 9 - .../UpsertFeature/ResponseStructs.swift | 19 +- .../UpsertFeature/ViewController.swift | 48 +- Samples/Validations/Podfile | 3 +- 42 files changed, 408 insertions(+), 1994 deletions(-) delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.pbxproj delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 Samples/GetSample/GetSample/AppDelegate.swift delete mode 100644 Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Samples/GetSample/GetSample/Assets.xcassets/Contents.json delete mode 100644 Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard delete mode 100644 Samples/GetSample/GetSample/Base.lproj/Main.storyboard delete mode 100644 Samples/GetSample/GetSample/ExampleAPICallback.swift delete mode 100644 Samples/GetSample/GetSample/ExampleTokenProvider.swift delete mode 100644 Samples/GetSample/GetSample/Info.plist delete mode 100644 Samples/GetSample/GetSample/ResponseStructs.swift delete mode 100644 Samples/GetSample/GetSample/SceneDelegate.swift delete mode 100644 Samples/GetSample/GetSample/ViewController.swift delete mode 100644 Samples/GetSample/GetSampleTests/GetSampleTests.swift delete mode 100644 Samples/GetSample/GetSampleUITests/GetSampleUITests.swift delete mode 100644 Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift delete mode 100644 Samples/GetSample/Podfile diff --git a/README.md b/README.md index 2f68ddbe..d4aa926a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # skyflow-iOS --- -Skyflow’s iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. +Skyflow's iOS SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-ios/actions) [![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-ios.svg)](https://github.com/skyflowapi/skyflow-ios/releases) @@ -136,7 +136,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env --- # Securely collecting data client-side -- [**Inserting data into the vault**](#insert-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) - [**Using validations on Collect Elements**](#validations) @@ -144,75 +143,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env - [**UI Error for Collect Elements**](#ui-error-for-collect-elements) - [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) -## Insert data into the vault - - To insert data into your vault, use the `skyflowClient.insert()` method. - ```swift - insert(records: [String: Any], options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback) - ``` - The `insert()` method requires a records parameter. - The `records` parameter takes an array of records to insert into the vault. - -#### Insert call example - ```swift - let insertCallback = InsertCallback() //A Custom callback using Skyflow.Callback - skyflowClient.insert( - records: [ - "records": [ - [ - "table": "cards", - "fields": [ - "cardNumber": "41111111111", - "cvv": "123", - ] - ] - ] - ], - callback: insertCallback -) -``` -Skyflow returns tokens for the record you just inserted. -```json -{ - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f37186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - }] -} -``` - - The `options` parameter takes a Skyflow.InsertOptions object. - - InsertOptions includes a `tokens` boolean that controls whether you receive tokens after inserting a record into the vault. - -InsertOptions also supports the **upsert** feature, that lets you conditionally insert or update an existing record by specifying the unique column to use as the unique value. - -For example, if you specify the ‘customer_id’ column to use for upsert, and the same customer_id already exists, the existing record will update. If the customer_id doesn't exist, the vault creates a new record. - -#### Insert call example -```swift -let records = [ - "records" : [ - [ - "table": "customers", //The table where you are inserting the record. - "fields": [ - "name" : "Francis", - "customer_id" : "12345" - ] - ] - ] -] - -let upsertOptions = [["table": "customers", "column": "customer_id"]] as [[String : Any]] -let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) -let insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback - -skyflowClient.insert(records: records, options: insertOptions, callback: insertCallback) -``` - ## 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. @@ -451,18 +381,20 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { ``` #### Step 4 : Collect data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: +When you submit the form, call the `collect(callback: Skyflow.CollectCallback, options: Skyflow.CollectOptions? = nil)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: ```swift // Non-PCI records let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in print(response) }, + onFailure: { error in print(error) } +) container?.collect(callback: insertCallback, options: options) ``` @@ -517,44 +449,75 @@ let skyflowElement = container?.create(input: input, options: requiredOption) let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +// It parses onSuccess into a typed Skyflow.CollectResponse, and onFailure into a typed +// Skyflow.SkyflowAPIError whenever the vault returns that structured shape (see below); +// otherwise onFailure receives the raw error (e.g. a validation SkyflowError) unchanged. +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in + print(response) + }, + onFailure: { error in + print(error) + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) ``` #### Skyflow returns tokens for the record you just inserted: -``` +```json { - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", + "fields": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } - }] + ] +} +``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode` - see the [partial error response example](#sample-partial-error-response) below. + +#### If the entire request fails (for example, the vault itself can't be found): +```json +{ + "error": { + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] + } +} +``` +When you use `Skyflow.CollectCallback`, `onFailure` delivers this shape as a typed `Skyflow.SkyflowAPIError` instead of a raw dictionary: +```swift +onFailure: { error in + if let apiError = error as? Skyflow.SkyflowAPIError { + print(apiError.error.grpcCode, apiError.error.httpCode, apiError.error.message, apiError.error.httpStatus, apiError.error.details) + } else { + // Any other failure (e.g. a validation SkyflowError) is passed through unchanged. + print(error) + } } ``` @@ -631,19 +594,21 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a 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. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowID": "value"]]] // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in print(response) }, + onFailure: { error in print(error) } +) container?.collect(callback: insertCallback, options: options) ``` @@ -698,25 +663,22 @@ let skyflowElement = container?.create(input: input, options: requiredOption) let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882"]] //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +// See "If the entire request fails" above for how onFailure surfaces a typed Skyflow.SkyflowAPIError. +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in + print(response) + }, + onFailure: { error in + print(error) + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) @@ -728,19 +690,21 @@ container?.collect(callback: insertCallback, options: collectOptions) { "records": [ { - "table": "persons", + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 }, { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" - } + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } ] } @@ -1212,19 +1176,21 @@ The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `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. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in print(response) }, + onFailure: { error in print(error) } +) container?.collect(callback: insertCallback, options: options) ``` End to end example of collecting data with Composable Elements @@ -1308,43 +1274,47 @@ do { let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] //Upsert options -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in + print(response) + }, + onFailure: { error in + print(error) + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) ``` ### Sample Response: -``` +```json { - "records": [ { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", + "fields": { + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } - }] + ] } ``` [For information on validations, see validations.](#validations) @@ -1670,19 +1640,21 @@ The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `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. +- `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] // Upsert -let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] +let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Custom callback - implementation of Skyflow.callback -let insertCallback = InsertCallback() +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in print(response) }, + onFailure: { error in print(error) } +) container?.collect(callback: insertCallback, options: options) ``` End to end example of collecting data with Composable Elements @@ -1769,23 +1741,20 @@ do { let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf"]] //Upsert options - let upsertOptions = [["table": "cards", "column": "cardNumber"]] as [[String : Any]] + let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) -//Implement a custom Skyflow.Callback to call on Insertion success/failure. -public class InsertCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { - print(error) - } -} - -// Initialize custom Skyflow.Callback. -let insertCallback = InsertCallback() +// Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). +let insertCallback = Skyflow.CollectCallback( + onSuccess: { response in + print(response) + }, + onFailure: { error in + print(error) + } +) // Call collect method on CollectContainer. container?.collect(callback: insertCallback, options: collectOptions) @@ -1795,250 +1764,70 @@ container?.collect(callback: insertCallback, options: collectOptions) { "records": [ { - "table": "persons", + "tableName": "persons", + "skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf", "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } + "gender": [{"token": "12f670af-6c7d-4837-83fb-30365fbc0b1e", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 }, { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 } ] } ``` ### Sample Partial Error Response: +Successful and failed records are both returned together in the same `records` array, each carrying its own `httpCode`: ```json { "records": [ { - "table": "cards", + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ], - "errors": [ + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}], + "first_name": [{"token": "131e70dc-6f76-4319-bdd3-96281e051051", "tokenGroupName": "deterministic_string"}], + "cvv": [{"token": "098834fe-de99-4fc8-abdf-88c18a28a2cf", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, { - "error": { - "code": 400, - "message": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec" - } + "error": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 } ] } ``` +If the entire request fails (for example, the vault itself can't be found), `onFailure` receives a single structured error instead of a `records` array: +```json +{ + "error": { + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] + } +} +``` + # 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 Skyflow tokens - To retrieve record data using tokens, use the `detokenize(records)` method. The records parameter takes a Dictionary object that contains tokens for `record` values to fetch - ```swift - [ - "records": [ - [ - "token": String, - "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. - ] - ] - ] - ``` - 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: - -```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let records = [ - "records": [ - [ - "token": "45012507-f72b-4f5c-9bf9-86b133bae719", - ], - [ - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "redaction": Skyflow.RedactionType.MASKED - ] - ] - ] as [String: Any] - - skyflowClient.detokenize(records: records, callback: getCallback) - ``` - The sample response: - ```json - { - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "1990-01-01" - }, - { - "token": "1r434532-6f76-4319-bdd3-96281e051051", - "value": "xxxxxxer", - } - ] - } - ``` - -- ### 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 Dictionary object that contains `records` to be fetched as shown below. 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. - - ```swift - [ - "records": [ - [ - "ids": ArrayList(), // 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 - ], - [ - "columnValues": ArrayList(), // Array of column values of the records to be fetched - "table": String, // Name of table holding the above skyflow_id's - "columnName": String, // A unique column name - "redaction": Skyflow.RedactionType // Redaction to be applied to retrieved data - ], - ] - ] - ``` - ### Redaction Types - There are 4 accepted values in Skyflow.RedactionTypes: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - An example of get call to fetch records: - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let columnValues = ["12345"] - let columnRecord = ["columnValues": columnValues,"columnName": "card_pin", "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record,columnRecord, badRecord]] - - skyflowClient.get(records: records, callback: getCallback) - ``` - - The sample response: - ```json - { - "records": [{ - "fields": { - "card_number": "4111111111111111", - "cvv": "127", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - },{ - "fields": { - "card_number": "4111111111111111", - "cvv": "345", - "card_pin":"12345" - "expiry_date": "12/29", - "fullname": "joey", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - }] - } - ``` - - An example of get call to fetch tokens: - - ```swift - let getCallback = GetCallback() // Custom callback - implementation of Skyflow.Callback - - let skyflowIDs = ["f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9", "da26de53-95d5-4bdb-99db-8d8c66a35ff9"] - let record = ["ids": skyflowIDs, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "cards", "redaction": Skyflow.RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - - skyflowClient.get(records: records, options: GetOptions(tokens: 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", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "0294-3213-3157-9802", - "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", - "id": "ed2e7851-9f84-4d70-9e5d-182f6d8d4fa3" - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - } - ] -} - ``` - - ## 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 @@ -2058,12 +1847,13 @@ let revealElementInput = Skyflow.RevealElementInput( 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 it is not provided - redaction: Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED + redaction: String, // optional. Redaction to apply for the element's token group. E.g. "MASKED" + tokenGroupName: String, // optional. Name of the token group the token belongs to. ) ``` `Note`: - `token` is optional only if it is being used in invokeConnection() -- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). +- `redaction` and `tokenGroupName` work together: when both are provided, the redaction is applied to that token group as part of the detokenize call. If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. 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. @@ -2140,7 +1930,10 @@ Elements used for revealing data are mounted to the screen the same way as Eleme ### 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: ```swift -let revealCallback = RevealCallback() // Custom callback - implementation of Skyflow.Callback +let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print(response) }, + onFailure: { error in print(error) } +) container.reveal(callback: revealCallback) ``` @@ -2189,7 +1982,8 @@ let cardNumberInput = Skyflow.RevealElementInput( errorTextStyles: errorTextStyles, label: "cardnumber", altText: "XXXX XXXX XXXX XXXX", - redaction: SKyflow.RedactionType.MASKED + redaction: "MASKED", + tokenGroupName: "deterministic_string" ) let cardNumberElement = container?.create(input: cardNumberInput) @@ -2221,18 +2015,15 @@ cvvElement!.setError("custom error") // reset error to the element cvvElement!.resetError() -// Implement a custom Skyflow.Callback to be called on Reveal success/failure -public class RevealCallback: Skyflow.Callback { - public func onSuccess(_ responseBody: Any) { - print(responseBody) - } - public func onFailure(_ error: Any) { +// Initialize a Skyflow.RevealCallback - required by RevealContainer's reveal(callback:options:). +let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in + print(response) + }, + onFailure: { error in print(error) } -} - -// Initialize custom Skyflow.Callback -let revealCallback = RevealCallback() +) // Call reveal method on RevealContainer container?.reveal(callback: revealCallback) @@ -2244,21 +2035,26 @@ The response below shows that some tokens assigned to the reveal elements get re #### Sample Response: ```json { - "success": [ { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" - }, - { - "token": "89024714-6a26-4256-b9d4-55ad69aa4047" - }], - "errors": [ { - "id": "a4b24714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047" + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "value": "4111111111111111", + "tokenGroupName": "deterministic_string" + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "value": "123", + "tokenGroupName": "deterministic_string" + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 } - }] + ] } ``` +**Note:** Successful and failed tokens are both returned in the same `records` array, each entry distinguished by the presence of an `error` key. ## Reporting a Vulnerability diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 48368c2d..f5efde30 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -127,23 +127,23 @@ class CollectAndRevealViewController: UIViewController { stackView.addArrangedSubview(collectExpMonth!) stackView.addArrangedSubview(collectExpYear!) stackView.addArrangedSubview(collectButton) - + stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 5 stackView.alignment = .fill stackView.translatesAutoresizingMaskIntoConstraints = false - + let scrollView = UIScrollView(frame: .zero) scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) - + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2).isActive = true scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 2).isActive = true scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - + scrollView.addSubview(stackView) stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true @@ -153,16 +153,27 @@ class CollectAndRevealViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let recordError = result.error { + print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + } + } + if let fields = response.records.first?.record?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { @@ -172,7 +183,12 @@ class CollectAndRevealViewController: UIViewController { } print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -189,39 +205,42 @@ class CollectAndRevealViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token, inputStyles: revealStyles, label: "Card Number", - redaction: .REDACTED + redaction: "REDACTED", + tokenGroupName: self.firstToken(tokens, column: "card_number").tokenGroupName ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, label: "CVV", - redaction: .MASKED + redaction: "MASKED", + tokenGroupName: self.firstToken(tokens, column: "cvv").tokenGroupName ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token, inputStyles: revealStyles, label: "Card Holder Name", - redaction: .DEFAULT - + redaction: "DEFAULT", + tokenGroupName: self.firstToken(tokens, column: "cardholder_name").tokenGroupName ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token, inputStyles: revealStyles, label: "Expiration Month", - redaction: .PLAIN_TEXT + redaction: "PLAIN_TEXT", + tokenGroupName: self.firstToken(tokens, column: "expiry_month").tokenGroupName ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token, inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift index 0d8be466..12e3bd84 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see CollectAndRevealViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift index 959044f5..67c28811 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/ResponseStructs.swift @@ -2,22 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/CollectAndRevealSample/Podfile b/Samples/CollectAndRevealSample/Podfile index a02fa6bd..ddb60497 100644 --- a/Samples/CollectAndRevealSample/Podfile +++ b/Samples/CollectAndRevealSample/Podfile @@ -5,9 +5,8 @@ source 'https://github.com/CocoaPods/Specs.git' target 'CollectAndRevealSample' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.23.0" + pod 'Skyflow', ">= 1.25.0" end diff --git a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift b/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift index 0d8be466..548a8e4c 100644 --- a/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift +++ b/Samples/ComposableElements/ComposableElements/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift b/Samples/ComposableElements/ComposableElements/ResponseStructs.swift index 959044f5..67c28811 100644 --- a/Samples/ComposableElements/ComposableElements/ResponseStructs.swift +++ b/Samples/ComposableElements/ComposableElements/ResponseStructs.swift @@ -2,22 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index 5597e404..d88b37f1 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -170,16 +170,43 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + + // Upsert example: insert-or-update credit_cards records, matching on card_number. + // If a record with a matching card_number already exists, its fields are merged + // in (updateType: .UPDATE) instead of creating a duplicate row. + let upsertOptions = [ + Skyflow.UpsertOption(table: "credit_cards", uniqueColumns: ["card_number"], updateType: .UPDATE) + ] + + // additionalFields: extra records submitted alongside whatever's collected from + // the mounted elements, one entry per table. + let additionalFields: [String: Any] = [ + "records": [ + ["table": "credit_cards", "fields": ["billing_zip": "94105"]] + ] + ] + + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true, additionalFields: additionalFields, upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let recordError = result.error { + print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + } + } + if let fields = response.records.first?.record?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { @@ -189,7 +216,12 @@ class ViewController: UIViewController { } print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -206,39 +238,42 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: self.firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, label: "Card Number", - redaction: .REDACTED + redaction: "REDACTED", + tokenGroupName: self.firstToken(tokens, column: "card_number").tokenGroupName ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: self.firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, label: "CVV", - redaction: .MASKED + redaction: "MASKED", + tokenGroupName: self.firstToken(tokens, column: "cvv").tokenGroupName ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: self.firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, label: "Card Holder Name", - redaction: .DEFAULT - + redaction: "DEFAULT", + tokenGroupName: self.firstToken(tokens, column: "cardholder_name").tokenGroupName ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: self.firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, label: "Expiration Month", - redaction: .PLAIN_TEXT + redaction: "PLAIN_TEXT", + tokenGroupName: self.firstToken(tokens, column: "expiry_month").tokenGroupName ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: self.firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) diff --git a/Samples/ComposableElements/Podfile b/Samples/ComposableElements/Podfile index 9d8d3f0e..77ef17f8 100644 --- a/Samples/ComposableElements/Podfile +++ b/Samples/ComposableElements/Podfile @@ -4,8 +4,7 @@ target 'ComposableElements' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for ComposableElements target 'ComposableElementsTests' do diff --git a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj b/Samples/GetSample/GetSample.xcodeproj/project.pbxproj deleted file mode 100644 index bdef97b9..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.pbxproj +++ /dev/null @@ -1,640 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */; }; - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */; }; - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */; }; - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */; }; - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */; }; - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */; }; - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */; }; - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */; }; - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */; }; - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */; }; - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */; }; - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */; }; - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */ = {isa = PBXBuildFile; productRef = 7C7BCD532A49AD6400C6FBCF /* Skyflow */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; - 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7C7BCD112A49A5B900C6FBCF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7C7BCD182A49A5B900C6FBCF; - remoteInfo = GetSample; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GetSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 7C7BCD232A49A5B900C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 7C7BCD282A49A5BA00C6FBCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleTests.swift; sourceTree = ""; }; - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GetSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITests.swift; sourceTree = ""; }; - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSampleUITestsLaunchTests.swift; sourceTree = ""; }; - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAPICallback.swift; sourceTree = ""; }; - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseStructs.swift; sourceTree = ""; }; - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTokenProvider.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 7C7BCD162A49A5B900C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD542A49AD6400C6FBCF /* Skyflow in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 7C7BCD102A49A5B900C6FBCF = { - isa = PBXGroup; - children = ( - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */, - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */, - 7C7BCD1A2A49A5B900C6FBCF /* Products */, - ); - sourceTree = ""; - }; - 7C7BCD1A2A49A5B900C6FBCF /* Products */ = { - isa = PBXGroup; - children = ( - 7C7BCD192A49A5B900C6FBCF /* GetSample.app */, - 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */, - 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 7C7BCD1B2A49A5B900C6FBCF /* GetSample */ = { - isa = PBXGroup; - children = ( - 7C7BCD1C2A49A5B900C6FBCF /* AppDelegate.swift */, - 7C7BCD1E2A49A5B900C6FBCF /* SceneDelegate.swift */, - 7C7BCD202A49A5B900C6FBCF /* ViewController.swift */, - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */, - 7C7BCD252A49A5BA00C6FBCF /* Assets.xcassets */, - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */, - 7C7BCD2A2A49A5BA00C6FBCF /* Info.plist */, - 7C7BCD4C2A49A70900C6FBCF /* ExampleAPICallback.swift */, - 7C7BCD4E2A49A72900C6FBCF /* ResponseStructs.swift */, - 7C7BCD502A49A75900C6FBCF /* ExampleTokenProvider.swift */, - ); - path = GetSample; - sourceTree = ""; - }; - 7C7BCD322A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXGroup; - children = ( - 7C7BCD332A49A5BB00C6FBCF /* GetSampleTests.swift */, - ); - path = GetSampleTests; - sourceTree = ""; - }; - 7C7BCD3C2A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXGroup; - children = ( - 7C7BCD3D2A49A5BB00C6FBCF /* GetSampleUITests.swift */, - 7C7BCD3F2A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift */, - ); - path = GetSampleUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 7C7BCD182A49A5B900C6FBCF /* GetSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */; - buildPhases = ( - 7C7BCD152A49A5B900C6FBCF /* Sources */, - 7C7BCD162A49A5B900C6FBCF /* Frameworks */, - 7C7BCD172A49A5B900C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GetSample; - packageProductDependencies = ( - 7C7BCD532A49AD6400C6FBCF /* Skyflow */, - ); - productName = GetSample; - productReference = 7C7BCD192A49A5B900C6FBCF /* GetSample.app */; - productType = "com.apple.product-type.application"; - }; - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */; - buildPhases = ( - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */, - 7C7BCD2C2A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleTests; - productName = GetSampleTests; - productReference = 7C7BCD2F2A49A5BB00C6FBCF /* GetSampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */; - buildPhases = ( - 7C7BCD352A49A5BB00C6FBCF /* Sources */, - 7C7BCD362A49A5BB00C6FBCF /* Frameworks */, - 7C7BCD372A49A5BB00C6FBCF /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */, - ); - name = GetSampleUITests; - productName = GetSampleUITests; - productReference = 7C7BCD392A49A5BB00C6FBCF /* GetSampleUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 7C7BCD112A49A5B900C6FBCF /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; - TargetAttributes = { - 7C7BCD182A49A5B900C6FBCF = { - CreatedOnToolsVersion = 14.0; - }; - 7C7BCD2E2A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - 7C7BCD382A49A5BB00C6FBCF = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 7C7BCD182A49A5B900C6FBCF; - }; - }; - }; - buildConfigurationList = 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 7C7BCD102A49A5B900C6FBCF; - packageReferences = ( - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */, - ); - productRefGroup = 7C7BCD1A2A49A5B900C6FBCF /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 7C7BCD182A49A5B900C6FBCF /* GetSample */, - 7C7BCD2E2A49A5BB00C6FBCF /* GetSampleTests */, - 7C7BCD382A49A5BB00C6FBCF /* GetSampleUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 7C7BCD172A49A5B900C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD292A49A5BA00C6FBCF /* LaunchScreen.storyboard in Resources */, - 7C7BCD262A49A5BA00C6FBCF /* Assets.xcassets in Resources */, - 7C7BCD242A49A5B900C6FBCF /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2D2A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD372A49A5BB00C6FBCF /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 7C7BCD152A49A5B900C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD4F2A49A72900C6FBCF /* ResponseStructs.swift in Sources */, - 7C7BCD512A49A75900C6FBCF /* ExampleTokenProvider.swift in Sources */, - 7C7BCD212A49A5B900C6FBCF /* ViewController.swift in Sources */, - 7C7BCD1D2A49A5B900C6FBCF /* AppDelegate.swift in Sources */, - 7C7BCD4D2A49A70900C6FBCF /* ExampleAPICallback.swift in Sources */, - 7C7BCD1F2A49A5B900C6FBCF /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD2B2A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD342A49A5BB00C6FBCF /* GetSampleTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7C7BCD352A49A5BB00C6FBCF /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 7C7BCD3E2A49A5BB00C6FBCF /* GetSampleUITests.swift in Sources */, - 7C7BCD402A49A5BB00C6FBCF /* GetSampleUITestsLaunchTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 7C7BCD312A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD302A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; - 7C7BCD3B2A49A5BB00C6FBCF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7C7BCD182A49A5B900C6FBCF /* GetSample */; - targetProxy = 7C7BCD3A2A49A5BB00C6FBCF /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 7C7BCD222A49A5B900C6FBCF /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD232A49A5B900C6FBCF /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 7C7BCD272A49A5BA00C6FBCF /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7C7BCD282A49A5BA00C6FBCF /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 7C7BCD412A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 7C7BCD422A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 7C7BCD442A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 7C7BCD452A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = GetSample/Info.plist; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 7C7BCD472A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Debug; - }; - 7C7BCD482A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GetSample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GetSample"; - }; - name = Release; - }; - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Debug; - }; - 7C7BCD4B2A49A5BB00C6FBCF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.skyflow.InputFormatting.GetSampleUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = GetSample; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 7C7BCD142A49A5B900C6FBCF /* Build configuration list for PBXProject "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD412A49A5BB00C6FBCF /* Debug */, - 7C7BCD422A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD432A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD442A49A5BB00C6FBCF /* Debug */, - 7C7BCD452A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD462A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD472A49A5BB00C6FBCF /* Debug */, - 7C7BCD482A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 7C7BCD492A49A5BB00C6FBCF /* Build configuration list for PBXNativeTarget "GetSampleUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 7C7BCD4A2A49A5BB00C6FBCF /* Debug */, - 7C7BCD4B2A49A5BB00C6FBCF /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/skyflowapi/skyflow-iOS.git"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 7C7BCD532A49AD6400C6FBCF /* Skyflow */ = { - isa = XCSwiftPackageProductDependency; - package = 7C7BCD522A49AD6400C6FBCF /* XCRemoteSwiftPackageReference "skyflow-iOS" */; - productName = Skyflow; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 7C7BCD112A49A5B900C6FBCF /* Project object */; -} diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index f5762feb..00000000 --- a/Samples/GetSample/GetSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "aexml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tadija/AEXML.git", - "state" : { - "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version" : "4.6.1" - } - }, - { - "identity" : "skyflow-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/skyflowapi/skyflow-iOS.git", - "state" : { - "branch" : "main", - "revision" : "d746cff983d44dd4690780c14aa7ca60387d7850" - } - } - ], - "version" : 2 -} diff --git a/Samples/GetSample/GetSample/AppDelegate.swift b/Samples/GetSample/GetSample/AppDelegate.swift deleted file mode 100644 index aec625c4..00000000 --- a/Samples/GetSample/GetSample/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3e..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Assets.xcassets/Contents.json b/Samples/GetSample/GetSample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/Samples/GetSample/GetSample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard b/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/Samples/GetSample/GetSample/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/GetSample/GetSample/Base.lproj/Main.storyboard b/Samples/GetSample/GetSample/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/Samples/GetSample/GetSample/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/GetSample/GetSample/ExampleAPICallback.swift b/Samples/GetSample/GetSample/ExampleAPICallback.swift deleted file mode 100644 index 530d74fd..00000000 --- a/Samples/GetSample/GetSample/ExampleAPICallback.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// ExampleAPICallback.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} diff --git a/Samples/GetSample/GetSample/ExampleTokenProvider.swift b/Samples/GetSample/GetSample/ExampleTokenProvider.swift deleted file mode 100644 index 75a0efee..00000000 --- a/Samples/GetSample/GetSample/ExampleTokenProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation -import Skyflow - -public class ExampleTokenProvider: TokenProvider { - public func getBearerToken(_ apiCallback: Skyflow.Callback) { - if let url = URL(string: "") { - let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) { data, _, error in - if error != nil { - print(error!) - return - } - if let safeData = data { - do { - let x = try JSONSerialization.jsonObject(with: safeData, options: []) as? [String: String] - if let accessToken = x?["accessToken"] { - apiCallback.onSuccess(accessToken) - } - } catch { - print("access token wrong format") - } - } - } - task.resume() - } - } -} diff --git a/Samples/GetSample/GetSample/Info.plist b/Samples/GetSample/GetSample/Info.plist deleted file mode 100644 index dd3c9afd..00000000 --- a/Samples/GetSample/GetSample/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - - diff --git a/Samples/GetSample/GetSample/ResponseStructs.swift b/Samples/GetSample/GetSample/ResponseStructs.swift deleted file mode 100644 index 959044f5..00000000 --- a/Samples/GetSample/GetSample/ResponseStructs.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let skyflow_id: String -} diff --git a/Samples/GetSample/GetSample/SceneDelegate.swift b/Samples/GetSample/GetSample/SceneDelegate.swift deleted file mode 100644 index a968b5bc..00000000 --- a/Samples/GetSample/GetSample/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// GetSample -// -// Created by Bharti Sagar on 26/06/23. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - diff --git a/Samples/GetSample/GetSample/ViewController.swift b/Samples/GetSample/GetSample/ViewController.swift deleted file mode 100644 index df14f26a..00000000 --- a/Samples/GetSample/GetSample/ViewController.swift +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow - */ - -import UIKit -import Skyflow - -class ViewController: UIViewController { - private var skyflow: Skyflow.Client? - private var stackView: UIStackView! - - override func loadView() { - let view = UIView() - view.backgroundColor = .white - view.translatesAutoresizingMaskIntoConstraints = false - self.view = view - } - - override func viewDidLoad() { - super.viewDidLoad() - - let tokenProvider = ExampleTokenProvider() - self.stackView = UIStackView() - - let config = Skyflow.Configuration( - vaultID: "", - vaultURL: "", - tokenProvider: tokenProvider, - options: Skyflow.Options( - logLevel: Skyflow.LogLevel.DEBUG - ) - ) - - self.skyflow = Skyflow.initialize(config) - - if self.skyflow != nil { - let getWithRedactionType = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithRedactionType.backgroundColor = .gray - getWithRedactionType.setTitle("Get Records With Redaction Type", for: .normal) - getWithRedactionType.addTarget(self, action: #selector(getRecordsWithRedactionType), for: .touchUpInside) - - let getWithGetOptionTokens = UIButton(frame: CGRect(x: 100, y: 400, width: 100, height: 70)) - getWithGetOptionTokens.backgroundColor = .gray - getWithGetOptionTokens.setTitle("Get Records with tokens", for: .normal) - getWithGetOptionTokens.addTarget(self, action: #selector(getRecordsWithGetOptionTokens), for: .touchUpInside) - - stackView.addArrangedSubview(getWithRedactionType) - stackView.addArrangedSubview(getWithGetOptionTokens) - - stackView.axis = .vertical - stackView.distribution = .fill - stackView.spacing = 10 - stackView.alignment = .fill - stackView.translatesAutoresizingMaskIntoConstraints = false - let scrollView = UIScrollView(frame: .zero) - scrollView.isScrollEnabled = true - scrollView.backgroundColor = .white - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - scrollView.topAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.topAnchor, - constant: 20 - ).isActive = true - scrollView.leftAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.leftAnchor, - constant: 10 - ).isActive = true - scrollView.rightAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.rightAnchor, - constant: -10 - ).isActive = true - scrollView.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor - ).isActive = true - scrollView.addSubview(stackView) - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -10).isActive = true - stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true - stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true - stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -10).isActive = true - stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true - } - } - @objc func getRecordsWithRedactionType() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": "", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - let recordColumn = ["columnValues": ["", ""],"table": "", "columnName": "","redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, recordColumn, badRecord]] - self.skyflow?.get(records: records, callback: exampleAPICallback) - } - - @objc func getRecordsWithGetOptionTokens() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let record = ["ids": ["", ""], "table": ""] as [String : Any] - - let invalidID = ["invalid skyflow ID"] - let badRecord = ["ids": invalidID, "table": "table", "redaction": RedactionType.PLAIN_TEXT] as [String : Any] - - let records = ["records": [record, badRecord]] - self.skyflow?.get(records: records, options: GetOptions(tokens: true), callback: exampleAPICallback) - } - internal func updateSuccess(_ response: SuccessResponse) { - print(response) - print("Successfully got response:", response) - } - internal func updateFailure(error: Any) { - print("Failed Operation", error) - } -} diff --git a/Samples/GetSample/GetSampleTests/GetSampleTests.swift b/Samples/GetSample/GetSampleTests/GetSampleTests.swift deleted file mode 100644 index 4837e722..00000000 --- a/Samples/GetSample/GetSampleTests/GetSampleTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// GetSampleTests.swift -// GetSampleTests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest -@testable import GetSample - -final class GetSampleTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift deleted file mode 100644 index 8b26e9ab..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// GetSampleUITests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift b/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift deleted file mode 100644 index 75f3db9b..00000000 --- a/Samples/GetSample/GetSampleUITests/GetSampleUITestsLaunchTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// GetSampleUITestsLaunchTests.swift -// GetSampleUITests -// -// Created by Bharti Sagar on 26/06/23. -// - -import XCTest - -final class GetSampleUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/Samples/GetSample/Podfile b/Samples/GetSample/Podfile deleted file mode 100644 index 72b31da4..00000000 --- a/Samples/GetSample/Podfile +++ /dev/null @@ -1,12 +0,0 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -source 'https://github.com/CocoaPods/Specs.git' - -target 'GetSample' do - #use_frameworks! - pod 'AEXML', '4.6.1' - # Pods for GetSample - pod 'Skyflow', ">= 1.22.0" - -end diff --git a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift b/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift index 25f84e24..548a8e4c 100644 --- a/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift +++ b/Samples/InputFormatting/InputFormatting/ExampleAPICallback.swift @@ -1,50 +1,7 @@ /* * Copyright (c) 2022 Skyflow */ -// Created by Bharti Sagar on 26/04/23. -// -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (SuccessResponse) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift b/Samples/InputFormatting/InputFormatting/ResponseStructs.swift index 2230b92f..67c28811 100644 --- a/Samples/InputFormatting/InputFormatting/ResponseStructs.swift +++ b/Samples/InputFormatting/InputFormatting/ResponseStructs.swift @@ -2,28 +2,6 @@ * Copyright (c) 2022 Skyflow */ -// Created by Bharti Sagar on 26/04/23. -// - -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardholder_name: String - let card_number: String - let expiry_month: String - let expiry_year: String - let cvv: String - let ssn: String - let phone_number: String - let license_number: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/InputFormatting/InputFormatting/ViewController.swift b/Samples/InputFormatting/InputFormatting/ViewController.swift index fbacc971..206847b7 100644 --- a/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -201,21 +201,37 @@ class ViewController: UIViewController { } } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) - updateRevealInputs(tokens: response.records[0].fields) + for result in response.records { + if let recordError = result.error { + print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + } + } + if let fields = response.records.first?.record?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { print("Failed Operation", error) } - internal func updateRevealInputs(tokens: Fields) { + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) + } + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -232,7 +248,7 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.card_number, + token: firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, label: "Card Number" ) @@ -241,43 +257,43 @@ class ViewController: UIViewController { options: Skyflow.RevealElementOptions(format: "XXXX-XXXX-XXXX-XXXX-XXX", translation: ["X": "[0-9]"]) ) let revealCVVtInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( - token: tokens.cardholder_name, + token: firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( - token: tokens.expiry_month, + token: firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( - token: tokens.expiry_year, + token: firstToken(tokens, column: "expiry_year").token ?? "", inputStyles: revealStyles, label: "Expiration Year" ) self.revealExpirationYear = self.revealContainer?.create(input: revealExpirationYearInput) let revealSSNInput = Skyflow.RevealElementInput( - token: tokens.ssn, + token: firstToken(tokens, column: "ssn").token ?? "", inputStyles: revealStyles, label: "SSN" ) self.revealSSN = self.revealContainer?.create(input: revealSSNInput, options: Skyflow.RevealElementOptions(format: "XXX XX XXXX", translation: ["X": "[0-9]"])) let revealPhoneNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "Phone Number" ) self.revealPhoneNumber = self.revealContainer?.create(input: revealPhoneNumberInput, options: Skyflow.RevealElementOptions(format: "+91 XXXX-XX-XXXX", translation: ["X": "[0-9]"])) let revealLicenseNumberInput = Skyflow.RevealElementInput( - token: tokens.phone_number, + token: firstToken(tokens, column: "phone_number").token ?? "", inputStyles: revealStyles, label: "License Number" ) diff --git a/Samples/InputFormatting/Podfile b/Samples/InputFormatting/Podfile index f4ddb57c..354055ad 100644 --- a/Samples/InputFormatting/Podfile +++ b/Samples/InputFormatting/Podfile @@ -3,9 +3,8 @@ target 'InputFormatting' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - pod 'AEXML', '4.6.1' - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" # Pods for InputFormatting diff --git a/Samples/InputFormatting/Podfile.lock b/Samples/InputFormatting/Podfile.lock index e9d81271..7592f10a 100644 --- a/Samples/InputFormatting/Podfile.lock +++ b/Samples/InputFormatting/Podfile.lock @@ -1,18 +1,14 @@ PODS: - - AEXML (4.6.1) - Skyflow (1.18.0-beta.1): - - AEXML DEPENDENCIES: - Skyflow (= 1.18.0-beta.1) SPEC REPOS: trunk: - - AEXML - Skyflow SPEC CHECKSUMS: - AEXML: 1e255ecc6597212f97a7454a69ebd3ede64ac1cf Skyflow: d702f8e9beafba05e164a3f74a6f862596f41085 PODFILE CHECKSUM: 4958176a88d5d7c9eb37117625a3937f5da81019 diff --git a/Samples/UpdateDataUsingElements/Podfile b/Samples/UpdateDataUsingElements/Podfile index 288940eb..b8ac5a1c 100644 --- a/Samples/UpdateDataUsingElements/Podfile +++ b/Samples/UpdateDataUsingElements/Podfile @@ -6,20 +6,17 @@ target 'UpdateDataUsingElements' do use_frameworks! # Pods for UpdateDataUsingElements - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' target 'UpdateDataUsingElementsTests' do inherit! :search_paths # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' end target 'UpdateDataUsingElementsUITests' do # Pods for testing - pod 'AEXML', '4.6.1' - pod 'Skyflow', '1.25.0' + pod 'Skyflow', '>=1.25.0' end diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift index b08d2305..548a8e4c 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ExampleAPICallback.swift @@ -2,47 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation -import Skyflow - -public class ExampleAPICallback: Skyflow.Callback { - private var updateSuccess: ((SuccessResponse) -> Void)? - private var updateFailure: ((Any) -> Void)? - private var decoder: JSONDecoder? - - internal init(updateSuccess: @escaping (Any) -> Void, updateFailure: @escaping (Any) -> Void) { - self.updateSuccess = updateSuccess - self.updateFailure = updateFailure - self.decoder = JSONDecoder() - } - - internal init() { - } - - public func onSuccess(_ responseBody: Any) { - if updateSuccess == nil { - print("success:", responseBody) - return - } - do { - let dataString = String(data: try! JSONSerialization.data(withJSONObject: responseBody), encoding: .utf8) - let responseData = Data(dataString!.utf8) - let jsonResponse = try decoder!.decode(SuccessResponse.self, from: responseData) - - updateSuccess!(jsonResponse) - - print("success:", jsonResponse) - } catch { - print("error deserializing", error) - } - } - - public func onFailure(_ error: Any) { - print(error) - if updateFailure != nil { - print("failure:", error) - return - } - print("onfailure", error) - } -} +// No longer needed - use Skyflow.CollectCallback / Skyflow.RevealCallback directly +// (see ViewController.swift), which already deliver typed responses +// (Skyflow.CollectResponse / Skyflow.RevealResponse) without a custom Callback subclass. diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift index 4b948ce6..67c28811 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ResponseStructs.swift @@ -2,23 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields? - let table: String - let skyflow_id: String? -} - -struct Fields: Codable { - let cardholder_name: String? - let card_number: String? - let expiry_month: String? - let expiry_year: String? - let cvv: String? - let skyflow_id: String? -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift index 6639ec61..f058aab0 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -164,12 +164,17 @@ class ViewController: UIViewController { } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) } - internal func updateSuccess(_ response: SuccessResponse) { + internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 + for result in response.records { + if let recordError = result.error { + print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + } + } print("Successfully got response:", response) } internal func updateFailure(error: Any) { diff --git a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 040de638..d02f09c6 100644 --- a/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Samples/UpsertFeature/UpsertFeature.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "AEXML", - "repositoryURL": "https://github.com/tadija/AEXML.git", - "state": { - "branch": null, - "revision": "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version": "4.6.1" - } - }, { "package": "Skyflow", "repositoryURL": "https://github.com/skyflowapi/skyflow-iOS.git", diff --git a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift b/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift index 6ea1a8a6..67c28811 100644 --- a/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift +++ b/Samples/UpsertFeature/UpsertFeature/ResponseStructs.swift @@ -2,19 +2,6 @@ * Copyright (c) 2022 Skyflow */ -import Foundation - -struct SuccessResponse: Codable { - let records: [Records] -} - -struct Records: Codable { - let fields: Fields - let table: String -} - -struct Fields: Codable { - let cardnumber: String - let cvv: String - let skyflow_id: String -} +// No longer needed - the SDK now provides typed responses directly +// (Skyflow.CollectResponse / Skyflow.RevealResponse), so this sample no longer +// needs to declare its own local response structs. diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 86f38902..f6469d44 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -40,7 +40,7 @@ class ViewController: UIViewController { textAlignment: .left, textColor: .blue ) - let focusStyle = Skyflow.Style(borderColor: .blue) ̰ + let focusStyle = Skyflow.Style(borderColor: .blue) let completedStyle = Skyflow.Style(borderColor: UIColor.green, textColor: UIColor.green) let invalidStyle = Skyflow.Style(borderColor: UIColor.red, textColor: UIColor.red) let styles = Skyflow.Styles( @@ -150,25 +150,43 @@ class ViewController: UIViewController { } @objc func revealForm() { - self.revealContainer?.reveal(callback: ExampleAPICallback()) + let revealCallback = Skyflow.RevealCallback( + onSuccess: { response in print("reveal success:", response) }, + onFailure: { error in print("reveal failure:", error) } + ) + self.revealContainer?.reveal(callback: revealCallback) } @objc func submitForm() { - let exampleAPICallback = ExampleAPICallback(updateSuccess: updateSuccess, updateFailure: updateFailure) - let upsertOptions = [["table": "persons", "column": "cardnumber"]] as [[String: Any]] - container!.collect(callback: exampleAPICallback, options: Skyflow.CollectOptions(tokens: true)) + let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) + let upsertOptions = [Skyflow.UpsertOption(table: "persons", uniqueColumns: ["cardnumber"], updateType: .UPDATE)] + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true, upsert: upsertOptions)) } - internal func updateSuccess(_ response: SuccessResponse) { - updateRevealInputs(tokens: response.records[0].fields) + internal func updateSuccess(_ response: Skyflow.CollectResponse) { + print(response) + for result in response.records { + if let recordError = result.error { + print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + } + } + if let fields = response.records.first?.record?.fields { + updateRevealInputs(tokens: fields) + } print("Successfully got response:", response) } - internal func updateFailure() { - print("Failed Operation") + internal func updateFailure(error: Any) { + print("Failed Operation", error) + } + + // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. + internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { + guard let entries = fields[column] as? [[String: Any]], let first = entries.first else { return ("", nil) } + return (first["token"] as? String ?? "", first["tokenGroupName"] as? String) } - internal func updateRevealInputs(tokens: Fields) { + internal func updateRevealInputs(tokens: [String: Any]) { let revealBaseStyle = Skyflow.Style( borderColor: UIColor.black, cornerRadius: 20, @@ -185,20 +203,22 @@ class ViewController: UIViewController { self.revealed = true } let revealCardNumberInput = Skyflow.RevealElementInput( - token: tokens.cardnumber, + token: firstToken(tokens, column: "cardnumber").token, inputStyles: revealStyles, label: "Card Number", - redaction: .DEFAULT + redaction: "DEFAULT", + tokenGroupName: firstToken(tokens, column: "cardnumber").tokenGroupName ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, options: Skyflow.RevealElementOptions() ) let revealCvvInput = Skyflow.RevealElementInput( - token: tokens.cvv, + token: firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, label: "Cvv", - redaction: .DEFAULT + redaction: "DEFAULT", + tokenGroupName: firstToken(tokens, column: "cvv").tokenGroupName ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/Samples/Validations/Podfile b/Samples/Validations/Podfile index cb757a40..e8fecb94 100644 --- a/Samples/Validations/Podfile +++ b/Samples/Validations/Podfile @@ -5,8 +5,7 @@ source 'https://github.com/CocoaPods/Specs.git' target 'Validations' do #use_frameworks! - pod 'AEXML', '4.6.1' # Pods for skyflow-iOS-sample-app - pod 'Skyflow', ">= 1.22.0" + pod 'Skyflow', ">= 1.25.0" end From 95576da503ab4ab116bf55797c29f6bc74b4ca5c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 28 Jul 2026 16:47:17 +0530 Subject: [PATCH 09/14] SK-3005 update collect and reveal strcuture --- README.md | 52 +++++------ .../CollectAndRevealViewController.swift | 8 +- .../ComposableElements/ViewController.swift | 8 +- .../InputFormatting/ViewController.swift | 8 +- .../ViewController.swift | 16 ++-- .../UpsertFeature/ViewController.swift | 8 +- .../Skyflow/collect/CollectContainer.swift | 4 +- Sources/Skyflow/collect/CollectOptions.swift | 4 +- .../Skyflow/collect/CollectRequestBody.swift | 28 +++--- Sources/Skyflow/collect/CollectResponse.swift | 66 +++----------- .../collect/FlowVaultCollectAPICallback.swift | 2 +- .../collect/FlowVaultCollectRequestBody.swift | 28 +++--- .../Skyflow/collect/FlowVaultICOptions.swift | 4 +- .../collect/FlowVaultInsertAPICallback.swift | 2 +- Sources/Skyflow/collect/InsertOptions.swift | 4 +- .../composable/ComposableContainer.swift | 2 +- Sources/Skyflow/core/Client.swift | 2 +- Sources/Skyflow/core/SkyflowAPIError.swift | 37 -------- Sources/Skyflow/elements/SkyflowElement.swift | 4 +- .../elements/core/CollectElementInput.swift | 14 +-- Sources/Skyflow/errors/ErrorCodes.swift | 79 ++++++++++++++++- Sources/Skyflow/reveal/RevealResponse.swift | 64 ++++---------- .../Skyflow/reveal/RevealValueCallback.swift | 86 ++++++++++++------- .../skyflow_iOS_collectTests.swift | 32 +++---- ...skyflow_iOS_composableEelementsTests.swift | 4 +- .../Skyflow_iOS_collectErrorTests.swift | 41 +++++++-- .../skyflow_iOS_collectUtilTests.swift | 14 +-- ...w_iOS_dormantCollectRequestBodyTests.swift | 4 +- .../skyflow_iOS_insertUtilTests 2.swift | 6 +- .../skyflow_iOS_revealUtilTests.swift | 13 +-- .../skyflow_iOS_utilTests.swift | 4 +- 31 files changed, 329 insertions(+), 319 deletions(-) delete mode 100644 Sources/Skyflow/core/SkyflowAPIError.swift diff --git a/README.md b/README.md index d4aa926a..6d5c9b85 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,7 @@ let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) let insertCallback = Skyflow.CollectCallback( onSuccess: { response in print(response) }, @@ -452,12 +452,12 @@ let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). // It parses onSuccess into a typed Skyflow.CollectResponse, and onFailure into a typed -// Skyflow.SkyflowAPIError whenever the vault returns that structured shape (see below); +// Skyflow.SkyflowError whenever the vault returns that structured shape (see below); // otherwise onFailure receives the raw error (e.g. a validation SkyflowError) unchanged. let insertCallback = Skyflow.CollectCallback( onSuccess: { response in @@ -509,15 +509,10 @@ container?.collect(callback: insertCallback, options: collectOptions) } } ``` -When you use `Skyflow.CollectCallback`, `onFailure` delivers this shape as a typed `Skyflow.SkyflowAPIError` instead of a raw dictionary: +`Skyflow.CollectCallback`/`Skyflow.RevealCallback`'s `onFailure` is typed `(Skyflow.SkyflowError) -> Void` - this shape (and every other failure, including client-side validation) is always delivered as a `Skyflow.SkyflowError`, with `httpCode`, `message`, `grpcCode`, `httpStatus`, and `details` all available as direct properties (`grpcCode`/`httpStatus`/`details` are only populated for this whole-request-failure shape - `nil` for validation failures): ```swift -onFailure: { error in - if let apiError = error as? Skyflow.SkyflowAPIError { - print(apiError.error.grpcCode, apiError.error.httpCode, apiError.error.message, apiError.error.httpStatus, apiError.error.details) - } else { - // Any other failure (e.g. a validation SkyflowError) is passed through unchanged. - print(error) - } +onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ``` @@ -547,7 +542,7 @@ let collectElementInput = Skyflow.CollectElementInput( altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. + skyflowId: String, // The skyflow_id of the record to be updated. ) ``` @@ -592,18 +587,17 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { ### Step 4 : Update data from Elements When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a 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`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowID": "value"]]] +let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowId": "value"]]] // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords) +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) let insertCallback = Skyflow.CollectCallback( onSuccess: { response in print(response) }, @@ -612,7 +606,7 @@ let insertCallback = Skyflow.CollectCallback( 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. +**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 ```swift @@ -648,7 +642,7 @@ let input = Skyflow.CollectElementInput( label: "card number", placeholder: "card number", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) // Create an option to require the element. @@ -660,17 +654,17 @@ let skyflowElement = container?.create(input: input, options: requiredOption) // Can interact with this object as a normal UIView Object and add to View // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882"]] +let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882"]] //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). -// See "If the entire request fails" above for how onFailure surfaces a typed Skyflow.SkyflowAPIError. +// See "If the entire request fails" above for how onFailure surfaces a typed Skyflow.SkyflowError. let insertCallback = Skyflow.CollectCallback( onSuccess: { response in print(response) @@ -1185,7 +1179,7 @@ let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) let insertCallback = Skyflow.CollectCallback( onSuccess: { response in print(response) }, @@ -1277,7 +1271,7 @@ let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). @@ -1586,7 +1580,7 @@ let composableElementInput = Skyflow.CollectElementInput( altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element type: Skyflow.ElementType, // Skyflow.ElementType enum - skyflowID: String, // The skyflow_id of the record to be updated. + skyflowId: String, // The skyflow_id of the record to be updated. ) ``` The `table` and `column` fields indicate which table and column in the vault the Element correspond to. @@ -1649,7 +1643,7 @@ let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) -let options = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) let insertCallback = Skyflow.CollectCallback( onSuccess: { response in print(response) }, @@ -1691,7 +1685,7 @@ let cardHolderNameElementInput = Skyflow.CollectElementInput( label: "first name", placeholder: "first name", type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) // Create an option to require the element. let requiredOption = Skyflow.CollectElementOptions(required: true) @@ -1709,7 +1703,7 @@ let cardNumberElementInput = Skyflow.CollectElementInput( label: "card number", placeholder: "card number", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) let cardNumberElement = container?.create(input: cardNumberElementInput, options: requiredOption) @@ -1723,7 +1717,7 @@ let cvvElementInput = Skyflow.CollectElementInput( label: "cvv", placeholder: "cvv", type: Skyflow.ElementType.CVV, - skyflowID: "431eaa6c-5c15-4513-aa15-29f50babe882" + skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882" ) let cvvElement = container?.create(input: cvvElementInput, options: requiredOption) @@ -1738,13 +1732,13 @@ do { } // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowID": "77dc3caf-c452-49e1-8625-07219d7567bf"]] +let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf"]] //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the Non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using optional field `upsert` of CollectOptions. -let collectOptions = Skyflow.CollectOptions(tokens: true, additionalFields: nonPCIRecords, upsert: upsertOptions) +let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). let insertCallback = Skyflow.CollectCallback( diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index f5efde30..07c53ec6 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -161,17 +161,17 @@ class CollectAndRevealViewController: UIViewController { } @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 for result in response.records { - if let recordError = result.error { - print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) } } - if let fields = response.records.first?.record?.fields { + if let fields = response.records.first?.fields { updateRevealInputs(tokens: fields) } print("Successfully got response:", response) diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index d88b37f1..5e863434 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -194,17 +194,17 @@ class ViewController: UIViewController { ] ] - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true, additionalFields: additionalFields, upsert: upsertOptions)) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(additionalFields: additionalFields, upsert: upsertOptions)) } internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 for result in response.records { - if let recordError = result.error { - print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) } } - if let fields = response.records.first?.record?.fields { + if let fields = response.records.first?.fields { updateRevealInputs(tokens: fields) } print("Successfully got response:", response) diff --git a/Samples/InputFormatting/InputFormatting/ViewController.swift b/Samples/InputFormatting/InputFormatting/ViewController.swift index 206847b7..fa93680c 100644 --- a/Samples/InputFormatting/InputFormatting/ViewController.swift +++ b/Samples/InputFormatting/InputFormatting/ViewController.swift @@ -209,16 +209,16 @@ class ViewController: UIViewController { } @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) for result in response.records { - if let recordError = result.error { - print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) } } - if let fields = response.records.first?.record?.fields { + if let fields = response.records.first?.fields { updateRevealInputs(tokens: fields) } print("Successfully got response:", response) diff --git a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift index f058aab0..cc18b7c8 100644 --- a/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift +++ b/Samples/UpdateDataUsingElements/UpdateDataUsingElements/ViewController.swift @@ -69,7 +69,7 @@ class ViewController: UIViewController { label: "Card Number", placeholder: "4111-1111-1111-1111", type: Skyflow.ElementType.CARD_NUMBER, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectNameInput = Skyflow.CollectElementInput( @@ -79,7 +79,7 @@ class ViewController: UIViewController { label: "Card Holder Name", placeholder: "John Doe", type: Skyflow.ElementType.CARDHOLDER_NAME, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectCVVInput = Skyflow.CollectElementInput( table: "credit_cards", @@ -88,7 +88,7 @@ class ViewController: UIViewController { label: "CVV", placeholder: "***", type: .CVV, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectExpMonthInput = Skyflow.CollectElementInput( table: "credit_cards", @@ -97,7 +97,7 @@ class ViewController: UIViewController { label: "Expiration Month", placeholder: "MM", type: .EXPIRATION_MONTH, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let collectExpYearInput = Skyflow.CollectElementInput( @@ -107,7 +107,7 @@ class ViewController: UIViewController { label: "Expiration Year", placeholder: "YYYY", type: .EXPIRATION_YEAR, - skyflowID: "" // replace it with actual skyflowID if you want to test update with elements functionality, otherwise you can remove skyflowID field from input + skyflowId: "" // replace it with actual skyflowId if you want to test update with elements functionality, otherwise you can remove skyflowId field from input ) let requiredOption = Skyflow.CollectElementOptions(required: true) @@ -165,14 +165,14 @@ class ViewController: UIViewController { @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true)) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions()) } internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) retryCount = 0 for result in response.records { - if let recordError = result.error { - print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) } } print("Successfully got response:", response) diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/UpsertFeature/UpsertFeature/ViewController.swift index f6469d44..c8e61015 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -160,17 +160,17 @@ class ViewController: UIViewController { @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) let upsertOptions = [Skyflow.UpsertOption(table: "persons", uniqueColumns: ["cardnumber"], updateType: .UPDATE)] - container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(tokens: true, upsert: upsertOptions)) + container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(upsert: upsertOptions)) } internal func updateSuccess(_ response: Skyflow.CollectResponse) { print(response) for result in response.records { - if let recordError = result.error { - print("Record failed:", recordError.error, "httpCode:", recordError.httpCode ?? -1) + if let error = result.error { + print("Record failed:", error, "httpCode:", result.httpCode) } } - if let fields = response.records.first?.record?.fields { + if let fields = response.records.first?.fields { updateRevealInputs(tokens: fields) } print("Successfully got response:", response) diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainer.swift index 0f8c8b4c..555003b8 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainer.swift @@ -59,7 +59,7 @@ public extension Container { } } if errors != "" { - callback.onFailure(NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) + callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) return } if options?.additionalFields != nil { @@ -85,7 +85,7 @@ public extension Container { } } let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = FlowVaultICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; diff --git a/Sources/Skyflow/collect/CollectOptions.swift b/Sources/Skyflow/collect/CollectOptions.swift index 6814cb45..2f4bbbff 100644 --- a/Sources/Skyflow/collect/CollectOptions.swift +++ b/Sources/Skyflow/collect/CollectOptions.swift @@ -7,11 +7,9 @@ import Foundation public struct CollectOptions { - var tokens: Bool var additionalFields: [String: Any]? var upsert: [UpsertOption]? - public init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil) { - self.tokens = tokens + public init(additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil) { self.additionalFields = additionalFields self.upsert = upsert } diff --git a/Sources/Skyflow/collect/CollectRequestBody.swift b/Sources/Skyflow/collect/CollectRequestBody.swift index b687f141..6f3d04a3 100644 --- a/Sources/Skyflow/collect/CollectRequestBody.swift +++ b/Sources/Skyflow/collect/CollectRequestBody.swift @@ -73,11 +73,11 @@ internal class CollectRequestBody { let entryDict = entry let tableName = entryDict["table"] as! String let fields = entryDict["fields"] as! [String: Any] - if entryDict["skyflowID"] != nil { + if entryDict["skyflowId"] != nil { // add to update payload - let skyflowID = entryDict["skyflowID"] as! String - if updatePayload[skyflowID] != nil { - let temp = updatePayload[skyflowID] as! [String: Any] + let skyflowId = entryDict["skyflowId"] as! String + if updatePayload[skyflowId] != nil { + let temp = updatePayload[skyflowId] as! [String: Any] // merge existing fields with new field var existingFields = temp["fields"] as! [String: Any] for (key, val) in fields { @@ -86,14 +86,14 @@ internal class CollectRequestBody { } var updatedTemp = temp updatedTemp["fields"] = existingFields - updatePayload[skyflowID] = updatedTemp + updatePayload[skyflowId] = updatedTemp } else { var temp: [String: Any] = [ "table": tableName, "fields": fields, - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } } else { if tableMap[tableName] != nil { @@ -126,11 +126,11 @@ internal class CollectRequestBody { let tableName = element.tableName! let columnName = element.columnName! let value = element.getValue() - let skyflowID = element.skyflowID // Assumes TextField has this property + let skyflowId = element.skyflowId // Assumes TextField has this property - if let skyflowID = skyflowID, !skyflowID.isEmpty { - if updatePayload[skyflowID] != nil { - var temp = updatePayload[skyflowID] as! [String: Any] + if let skyflowId = skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + var temp = updatePayload[skyflowId] as! [String: Any] // merge existing fields with new field var existingFields = temp["fields"] as! [String: Any] if existingFields[columnName] != nil { @@ -151,14 +151,14 @@ internal class CollectRequestBody { existingFields[columnName] = value } temp["fields"] = existingFields - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } else { var temp: [String: Any] = [ "table": tableName, "fields": [columnName: value], - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } } else { // Only add to payload diff --git a/Sources/Skyflow/collect/CollectResponse.swift b/Sources/Skyflow/collect/CollectResponse.swift index b2227c44..4aa6ee74 100644 --- a/Sources/Skyflow/collect/CollectResponse.swift +++ b/Sources/Skyflow/collect/CollectResponse.swift @@ -14,61 +14,26 @@ public struct CollectResponse { } } -// Each entry in "records" is either a successfully inserted/updated CollectRecordSuccess or a -// CollectRecordError describing why that entry failed - the vault returns both together in the same -// array, each tagged with its own httpCode. -public enum CollectRecord { - case success(CollectRecordSuccess) - case failure(CollectRecordError) - - init(_ dict: [String: Any]) { - if let error = dict["error"] as? String { - self = .failure(CollectRecordError(dict, error: error)) - } else { - self = .success(CollectRecordSuccess(dict)) - } - } - - public var record: CollectRecordSuccess? { - if case .success(let record) = self { return record } - return nil - } - - public var error: CollectRecordError? { - if case .failure(let error) = self { return error } - return nil - } -} - -public struct CollectRecordSuccess { +// Each entry in "records" is either a successfully inserted/updated record (error is nil) or a +// failed one (error is non-nil) - the vault returns both together in the same array, each tagged +// with its own httpCode. +public struct CollectRecord { public let tableName: String? public let skyflowID: String? // Keyed by column name, e.g. "card_number": [{"token": "...", "tokenGroupName": "..."}]. public let fields: [String: Any]? // Keyed by column name, e.g. "card_number": [{"data": "...", "hashName": "..."}]. public let hashedData: [String: Any]? - public let httpCode: Int? + public let httpCode: Int + public let error: String? init(_ dict: [String: Any]) { self.tableName = dict["tableName"] as? String self.skyflowID = dict["skyflowID"] as? String self.fields = dict["fields"] as? [String: Any] self.hashedData = dict["hashedData"] as? [String: Any] - self.httpCode = dict["httpCode"] as? Int - } -} - -public struct CollectRecordError { - public let error: String - public let skyflowID: String? - public let tableName: String? - public let httpCode: Int? - - init(_ dict: [String: Any], error: String) { - self.error = error - self.skyflowID = dict["skyflowID"] as? String - self.tableName = dict["tableName"] as? String - self.httpCode = dict["httpCode"] as? Int + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String } } @@ -76,28 +41,25 @@ public struct CollectRecordError { // for you. Use it directly with the original callback-based collect(callback:options:). public class CollectCallback: Callback { private let successHandler: (CollectResponse) -> Void - private let failureHandler: (Any) -> Void + private let failureHandler: (SkyflowError) -> Void - public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (Any) -> Void) { + public init(onSuccess: @escaping (CollectResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { self.successHandler = onSuccess self.failureHandler = onFailure } public func onSuccess(_ responseBody: Any) { guard let response = CollectResponse(responseBody) else { - failureHandler(responseBody) + failureHandler(SkyflowError.wrap(responseBody)) return } successHandler(response) } // Delivered when the entire request fails (e.g. vault not found, network error) rather - // than a per-record failure inside CollectResponse.records. + // than a per-record failure inside CollectResponse.records, and for client-side validation + // failures. Always normalized into a Skyflow.SkyflowError - see SkyflowError.wrap. public func onFailure(_ error: Any) { - guard let apiError = SkyflowAPIError(error) else { - failureHandler(error) - return - } - failureHandler(apiError) + failureHandler(SkyflowError.wrap(error)) } } diff --git a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift index 53065dcb..ebfdb0a9 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectAPICallback.swift @@ -275,7 +275,7 @@ internal class FlowVaultCollectAPICallback: Callback { if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } var fields: [String: Any] = [:] - if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { + if let tokens = entry["tokens"] as? [String: Any] { for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { fields[column] = tokenValue } diff --git a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift index bd1a231e..2e48cbd3 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift @@ -73,24 +73,24 @@ internal class FlowVaultCollectRequestBody { let entryDict = entry let tableName = entryDict["table"] as! String let fields = entryDict["fields"] as! [String: Any] - if entryDict["skyflowID"] != nil { - let skyflowID = entryDict["skyflowID"] as! String - if updatePayload[skyflowID] != nil { - let temp = updatePayload[skyflowID] as! [String: Any] + if entryDict["skyflowId"] != nil { + let skyflowId = entryDict["skyflowId"] as! String + if updatePayload[skyflowId] != nil { + let temp = updatePayload[skyflowId] as! [String: Any] var existingFields = temp["fields"] as! [String: Any] for (key, val) in fields { existingFields[key] = val } var updatedTemp = temp updatedTemp["fields"] = existingFields - updatePayload[skyflowID] = updatedTemp + updatePayload[skyflowId] = updatedTemp } else { let temp: [String: Any] = [ "table": tableName, "fields": fields, - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } continue } @@ -123,11 +123,11 @@ internal class FlowVaultCollectRequestBody { let tableName = element.tableName! let columnName = element.columnName! let value = element.getValue() - let skyflowID = element.skyflowID // Assumes TextField has this property + let skyflowId = element.skyflowId // Assumes TextField has this property - if let skyflowID = skyflowID, !skyflowID.isEmpty { - if updatePayload[skyflowID] != nil { - var temp = updatePayload[skyflowID] as! [String: Any] + if let skyflowId = skyflowId, !skyflowId.isEmpty { + if updatePayload[skyflowId] != nil { + var temp = updatePayload[skyflowId] as! [String: Any] var existingFields = temp["fields"] as! [String: Any] if existingFields[columnName] != nil { var hasElementValueMatchRule: Bool = false @@ -147,14 +147,14 @@ internal class FlowVaultCollectRequestBody { existingFields[columnName] = value } temp["fields"] = existingFields - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } else { let temp: [String: Any] = [ "table": tableName, "fields": [columnName: value], - "skyflowID": skyflowID + "skyflowId": skyflowId ] - updatePayload[skyflowID] = temp + updatePayload[skyflowId] = temp } } else { // Only add to payload diff --git a/Sources/Skyflow/collect/FlowVaultICOptions.swift b/Sources/Skyflow/collect/FlowVaultICOptions.swift index c11b4463..e36e1032 100644 --- a/Sources/Skyflow/collect/FlowVaultICOptions.swift +++ b/Sources/Skyflow/collect/FlowVaultICOptions.swift @@ -7,14 +7,12 @@ import Foundation internal struct FlowVaultICOptions { - var tokens: Bool var additionalFields: [String: Any]? var upsert: [UpsertOption]? var callback: Callback? var contextOptions: ContextOptions? - init(tokens: Bool = true, additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { - self.tokens = tokens + init(additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { self.additionalFields = additionalFields self.upsert = upsert self.callback = callback diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift index 6e360162..bae16313 100644 --- a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -274,7 +274,7 @@ internal class FlowVaultInsertAPICallback: Callback { if let skyflowID = entry["skyflowID"] { successEntry["skyflowID"] = skyflowID } if let tableName = entry["tableName"] { successEntry["tableName"] = tableName } var fields: [String: Any] = [:] - if self.options.tokens, let tokens = entry["tokens"] as? [String: Any] { + if let tokens = entry["tokens"] as? [String: Any] { for (column, tokenValue) in self.buildFieldsDict(dict: tokens) { fields[column] = tokenValue } diff --git a/Sources/Skyflow/collect/InsertOptions.swift b/Sources/Skyflow/collect/InsertOptions.swift index 6fc5725b..9f4f4301 100644 --- a/Sources/Skyflow/collect/InsertOptions.swift +++ b/Sources/Skyflow/collect/InsertOptions.swift @@ -7,10 +7,8 @@ import Foundation public struct InsertOptions { - var tokens: Bool var upsert: [UpsertOption]? - public init(tokens: Bool = true, upsert: [UpsertOption]? = nil) { - self.tokens = tokens + public init(upsert: [UpsertOption]? = nil) { self.upsert = upsert } } diff --git a/Sources/Skyflow/composable/ComposableContainer.swift b/Sources/Skyflow/composable/ComposableContainer.swift index 4e35b746..e5e35f66 100644 --- a/Sources/Skyflow/composable/ComposableContainer.swift +++ b/Sources/Skyflow/composable/ComposableContainer.swift @@ -269,7 +269,7 @@ public extension Container { } } let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) - let icOptions = FlowVaultICOptions(tokens: options!.tokens, additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(additionalFields: options?.additionalFields, upsert: options?.upsert, callback: callback, contextOptions: tempContextOptions) if options?.upsert != nil { if icOptions.validateUpsert() { return; diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index 0938da71..7505f499 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -34,7 +34,7 @@ public class Client { let errorCode = ErrorCodes.EMPTY_VAULT_URL() return callback.onFailure(errorCode.getErrorObject(contextOptions: tempContextOptions)) } - let icOptions = FlowVaultICOptions(tokens: options.tokens, upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) + let icOptions = FlowVaultICOptions(upsert: options.upsert, callback: callback, contextOptions: tempContextOptions) var errorCode: ErrorCodes? if records["records"] == nil { diff --git a/Sources/Skyflow/core/SkyflowAPIError.swift b/Sources/Skyflow/core/SkyflowAPIError.swift deleted file mode 100644 index 38619cea..00000000 --- a/Sources/Skyflow/core/SkyflowAPIError.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022 Skyflow -*/ - -// Shared structured-error shape delivered via Callback.onFailure when an entire Collect or -// Reveal request fails (e.g. vault not found, network error) rather than a per-record/per-token -// failure inside the corresponding records array. - -import Foundation - -public struct SkyflowAPIError { - // Shadows Swift's built-in Error protocol within this scope - use Swift.Error if ever - // needed here. - public struct Error { - public let grpcCode: Int? - public let httpCode: Int? - public let message: String? - public let httpStatus: String? - public let details: [Any]? - - init(_ dict: [String: Any]) { - self.grpcCode = dict["grpcCode"] as? Int - self.httpCode = dict["httpCode"] as? Int - self.message = dict["message"] as? String - self.httpStatus = dict["httpStatus"] as? String - self.details = dict["details"] as? [Any] - } - } - - public let error: Error - - public init?(_ error: Any) { - guard let dict = error as? [String: Any], - let detail = dict["error"] as? [String: Any] else { return nil } - self.error = Error(detail) - } -} diff --git a/Sources/Skyflow/elements/SkyflowElement.swift b/Sources/Skyflow/elements/SkyflowElement.swift index 0ac78fef..0ac7dd32 100644 --- a/Sources/Skyflow/elements/SkyflowElement.swift +++ b/Sources/Skyflow/elements/SkyflowElement.swift @@ -16,7 +16,7 @@ public class SkyflowElement: UIView { internal var fieldType: ElementType! internal var columnName: String! internal var tableName: String? - internal var skyflowID: String? + internal var skyflowId: String? internal var horizontalConstraints = [NSLayoutConstraint]() internal var verticalConstraint = [NSLayoutConstraint]() internal var collectInput: CollectElementInput! @@ -64,7 +64,7 @@ public class SkyflowElement: UIView { columnName = collectInput.column fieldType = collectInput.type isRequired = options.required - skyflowID = collectInput.skyflowID + skyflowId = collectInput.skyflowId } internal func getOutput() -> String? { diff --git a/Sources/Skyflow/elements/core/CollectElementInput.swift b/Sources/Skyflow/elements/core/CollectElementInput.swift index 419d6b8d..40527522 100644 --- a/Sources/Skyflow/elements/core/CollectElementInput.swift +++ b/Sources/Skyflow/elements/core/CollectElementInput.swift @@ -17,11 +17,11 @@ public struct CollectElementInput { var placeholder: String var type: ElementType? var validations: ValidationSet - var skyflowID: String + var skyflowId: String public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -31,12 +31,12 @@ public struct CollectElementInput { self.label = label! self.placeholder = placeholder! self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -47,13 +47,13 @@ public struct CollectElementInput { self.placeholder = placeholder! self.type = type self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } @available(*, deprecated, message: "altText param is deprecated") public init(table: String = "", column: String = "", inputStyles: Styles? = Styles(), labelStyles: Styles? = Styles(), errorTextStyles: Styles? = Styles(), iconStyles: Styles? = Styles(), label: String? = "", - placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowID: String? = "") { + placeholder: String? = "", altText: String? = "", type: ElementType?, validations: ValidationSet=ValidationSet(), skyflowId: String? = "") { self.table = table self.column = column self.inputStyles = inputStyles! @@ -64,6 +64,6 @@ public struct CollectElementInput { self.placeholder = placeholder! self.type = type self.validations = validations - self.skyflowID = skyflowID! + self.skyflowId = skyflowId! } } diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/Skyflow/errors/ErrorCodes.swift index e1a34a2d..d3c162a5 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/Skyflow/errors/ErrorCodes.swift @@ -133,4 +133,81 @@ internal enum ErrorCodes: CustomStringConvertible { } } -public class SkyflowError: NSError {} +public class SkyflowError: NSError { + // Only populated for a whole-request API failure (see init(apiError:)) - nil for + // client-side validation failures, which only have httpCode/message. + public let grpcCode: Int? + public let httpStatus: String? + public let details: [Any]? + + // Aliases for NSError's own code/localizedDescription, matching the API error JSON's key names. + public var httpCode: Int { self.code } + public var message: String { self.localizedDescription } + + public init(domain: String, code: Int, userInfo: [String: Any]? = nil, + grpcCode: Int? = nil, httpStatus: String? = nil, details: [Any]? = nil) { + self.grpcCode = grpcCode + self.httpStatus = httpStatus + self.details = details + super.init(domain: domain, code: code, userInfo: userInfo) + } + + public required init?(coder: NSCoder) { + self.grpcCode = coder.decodeObject(forKey: "grpcCode") as? Int + self.httpStatus = coder.decodeObject(forKey: "httpStatus") as? String + self.details = coder.decodeObject(forKey: "details") as? [Any] + super.init(coder: coder) + } + + // Builds a SkyflowError out of a whole-request API failure of the shape + // {"error": {"grpcCode", "httpCode", "message", "httpStatus", "details"}} - returns nil if + // apiError isn't in that shape (e.g. it's already a SkyflowError from client-side validation). + // httpCode maps to NSError's own `code`, and message maps to localizedDescription/NSLocalizedDescriptionKey. + public convenience init?(apiError: Any) { + guard let dict = apiError as? [String: Any], + let detail = dict["error"] as? [String: Any] else { return nil } + self.init( + domain: "", + code: detail["httpCode"] as? Int ?? 0, + userInfo: [NSLocalizedDescriptionKey: detail["message"] as? String ?? ""], + grpcCode: detail["grpcCode"] as? Int, + httpStatus: detail["httpStatus"] as? String, + details: detail["details"] as? [Any] + ) + } + + // Normalizes any Callback.onFailure payload into a SkyflowError, so CollectCallback/RevealCallback + // can type onFailure as (SkyflowError) -> Void instead of (Any) -> Void. + internal static func wrap(_ error: Any) -> SkyflowError { + if let skyflowError = error as? SkyflowError { + return skyflowError + } + if let apiError = SkyflowError(apiError: error) { + return apiError + } + if let nsError = error as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + // Client.detokenize() bypasses RevealValueCallback entirely, so whole-request failures + // arrive here still wrapped in an "errors" array instead of a raw NSError. Two shapes are + // possible depending on where the failure originated: + // - Client.swift's own pre-network validation (empty vaultID/vaultURL/records key): each + // entry in "errors" IS the NSError directly. + // - FlowVaultRevealAPICallback's post-dispatch failures (invalid bearer token, network + // error, invalid URL, etc.): each entry is {"error": }. + // Recover the underlying NSError instead of stringifying the whole container dict. + // (Container.reveal()'s equivalent failures are already unwrapped earlier, + // by RevealValueCallback.onFailure.) + if let dict = error as? [String: Any], let errorsArray = dict["errors"] as? [Any] { + let firstError = errorsArray.compactMap { entry -> NSError? in + if let nsError = entry as? NSError { return nsError } + if let nested = entry as? [String: Any] { return nested["error"] as? NSError } + return nil + }.first + if let firstError = firstError { + return SkyflowError(domain: firstError.domain, code: firstError.code, userInfo: firstError.userInfo) + } + } + return SkyflowError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error)"]) + } +} diff --git a/Sources/Skyflow/reveal/RevealResponse.swift b/Sources/Skyflow/reveal/RevealResponse.swift index 7941b016..2b7b91cd 100644 --- a/Sources/Skyflow/reveal/RevealResponse.swift +++ b/Sources/Skyflow/reveal/RevealResponse.swift @@ -25,56 +25,23 @@ public struct RevealResponse { } } -// Each entry in "records" is either a successfully revealed RevealRecordSuccess or a -// RevealRecordError describing why that token failed - the vault returns both together in the -// same array, each tagged with its own httpCode. -public enum RevealRecord { - case success(RevealRecordSuccess) - case failure(RevealRecordError) - - init(_ dict: [String: Any]) { - if let error = dict["error"] as? String { - self = .failure(RevealRecordError(dict, error: error)) - } else { - self = .success(RevealRecordSuccess(dict)) - } - } - - public var record: RevealRecordSuccess? { - if case .success(let record) = self { return record } - return nil - } - - public var error: RevealRecordError? { - if case .failure(let error) = self { return error } - return nil - } -} - -public struct RevealRecordSuccess { +// Each entry in "records" is either a successfully revealed token (error is nil) or a failed one +// (error is non-nil) - the vault returns both together in the same array, each tagged with its +// own httpCode. +public struct RevealRecord { public let token: String? public let tokenGroupName: String? // Keyed by whatever fields the vault includes (e.g. "skyflowID", "tableName"). public let metadata: [String: Any]? - public let httpCode: Int? + public let httpCode: Int + public let error: String? init(_ dict: [String: Any]) { self.token = dict["token"] as? String self.tokenGroupName = dict["tokenGroupName"] as? String self.metadata = dict["metadata"] as? [String: Any] - self.httpCode = dict["httpCode"] as? Int - } -} - -public struct RevealRecordError { - public let error: String - public let token: String? - public let httpCode: Int? - - init(_ dict: [String: Any], error: String) { - self.error = error - self.token = dict["token"] as? String - self.httpCode = dict["httpCode"] as? Int + self.httpCode = dict["httpCode"] as? Int ?? 0 + self.error = dict["error"] as? String } } @@ -84,27 +51,26 @@ public struct RevealRecordError { // RevealResponse type used for Client.detokenize() applies here too. public class RevealCallback: Callback { private let successHandler: (RevealResponse) -> Void - private let failureHandler: (Any) -> Void + private let failureHandler: (SkyflowError) -> Void - public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (Any) -> Void) { + public init(onSuccess: @escaping (RevealResponse) -> Void, onFailure: @escaping (SkyflowError) -> Void) { self.successHandler = onSuccess self.failureHandler = onFailure } public func onSuccess(_ responseBody: Any) { guard let response = RevealResponse(responseBody) else { - failureHandler(responseBody) + failureHandler(SkyflowError.wrap(responseBody)) return } successHandler(response) } // Delivered when the entire request fails (e.g. vault not found, network error) rather - // than a per-token failure inside RevealResponse.records, as a raw dictionary matching - // SkyflowAPIError's shape - construct one yourself via SkyflowAPIError(error) if needed. - // Note: RevealContainer.reveal()'s validation errors (empty vaultID, unmounted element, - // etc.) are raw NSError, not this shape. + // than a per-token failure inside RevealResponse.records, and for client-side validation + // failures (empty vaultID, unmounted element, etc.). Always normalized into a + // Skyflow.SkyflowError - see SkyflowError.wrap. public func onFailure(_ error: Any) { - failureHandler(error) + failureHandler(SkyflowError.wrap(error)) } } diff --git a/Sources/Skyflow/reveal/RevealValueCallback.swift b/Sources/Skyflow/reveal/RevealValueCallback.swift index b105af78..4c7e1d2b 100644 --- a/Sources/Skyflow/reveal/RevealValueCallback.swift +++ b/Sources/Skyflow/reveal/RevealValueCallback.swift @@ -63,47 +63,75 @@ internal class RevealValueCallback: Callback { self.clientCallback.onSuccess(response) } + // Reserved for whole-request failures: a genuinely opaque error (can't be parsed as a + // dictionary at all), or a network/API-level error not scoped to any specific token (no + // "token" key - e.g. an invalid bearer token, a connection failure). Only PER-TOKEN failures + // (each tagged with the "token" they belong to) are routed through onSuccess instead, matching + // Collect's convention of surfacing per-record issues via the record's own "error" field. func onFailure(_ error: Any) { - var response: [String: Any] = [:] - - if let responseJson = error as? [String: Any] { - var tokens: [String: String] = [:] - var records: [[String: Any]] = [] + guard let responseJson = error as? [String: Any] else { + self.clientCallback.onFailure(error) + return + } - if let responseRecords = responseJson["records"] as? [Any] { - for record in responseRecords { - guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } - let value = dict["value"] as? String - tokens[token] = value ?? token + var errors = [] as [[String: Any]] + if let responseErrors = responseJson["errors"] as? [[String: Any]] { + errors = responseErrors + } - records.append(dict) + // Not scoped to any specific reveal element - a genuine network/API-level failure, not a + // per-record issue, so it's delivered via onFailure rather than folded into the records array. + if let networkError = errors.first(where: { $0["token"] == nil }) { + DispatchQueue.main.async { + for revealElement in self.revealElements { + revealElement.hideError() } } + self.clientCallback.onFailure(networkError["error"] ?? error) + return + } + + var tokens: [String: String] = [:] + var records: [[String: Any]] = [] + + if let responseRecords = responseJson["records"] as? [Any] { + for record in responseRecords { + guard let dict = record as? [String: Any], let token = dict["token"] as? String else { continue } + let value = dict["value"] as? String + tokens[token] = value ?? token + + records.append(dict) + } + } - var errors = [] as [[String: Any]] - if let responseErrors = responseJson["errors"] as? [[String: Any]] { - errors = responseErrors + // Each entry's "error" arrives as an NSError here (built internally, not deserialized from + // JSON) - convert to a plain message string so it matches the "records" array's normal + // shape (each entry's "error" is always a String, whether from onSuccess or here). + let stringifiedErrors = errors.map { entry -> [String: Any] in + var updated = entry + if let nsError = entry["error"] as? NSError { + updated["error"] = nsError.localizedDescription } - records.append(contentsOf: errors) + return updated + } + records.append(contentsOf: stringifiedErrors) - response["records"] = records - let tokensToErrors = getTokensToErrors(errors) + let tokensToErrors = getTokensToErrors(errors) - DispatchQueue.main.async { - for revealElement in self.revealElements { - if let v = tokens[revealElement.revealInput.token]{ - revealElement.updateVal(value: v) - } - - let inputToken = revealElement.revealInput.token - revealElement.hideError() - if let errorMessage = tokensToErrors[inputToken] { - revealElement.showError(message: errorMessage) - } + DispatchQueue.main.async { + for revealElement in self.revealElements { + if let v = tokens[revealElement.revealInput.token]{ + revealElement.updateVal(value: v) + } + + let inputToken = revealElement.revealInput.token + revealElement.hideError() + if let errorMessage = tokensToErrors[inputToken] { + revealElement.showError(message: errorMessage) } } } - self.clientCallback.onFailure(response) + self.clientCallback.onSuccess(["records": records]) } func getTokensToErrors(_ errors: [[String: Any]]?) -> [String: String] { diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index a59050f8..58117c95 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -386,7 +386,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert with invalid token") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: ["records": records], options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: ["records": records], options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) @@ -749,7 +749,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -762,7 +762,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -777,7 +777,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -790,7 +790,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -803,7 +803,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -816,7 +816,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -829,7 +829,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -842,7 +842,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -855,7 +855,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) @@ -866,7 +866,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let container = skyflow.container(type: ContainerType.COLLECT) let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:[])) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:[])) wait(for: [expectation], timeout: 20.0) @@ -878,7 +878,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let upsertOptions = [UpsertOption(table: "card1", uniqueColumns: [])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) @@ -890,7 +890,7 @@ final class skyflow_iOS_collectTests: XCTestCase { let upsertOptions = [UpsertOption(table: "", uniqueColumns: ["person"])] let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(tokens: true, upsert:upsertOptions)) + container?.collect(callback: callback.asCollectCallback, options: CollectOptions(upsert:upsertOptions)) wait(for: [expectation], timeout: 20.0) @@ -910,7 +910,7 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: []) + let insertOptions = Skyflow.InsertOptions(upsert: []) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) @@ -931,7 +931,7 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) @@ -952,7 +952,7 @@ final class skyflow_iOS_collectTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index a92a171d..8fb2d3bb 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -467,7 +467,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { ]] ] let callback = DemoAPICallback(expectation: expectation) - let insertOptions = Skyflow.InsertOptions(tokens: false, upsert: upsertOptions) + let insertOptions = Skyflow.InsertOptions(upsert: upsertOptions) self.skyflow?.insert(records: records, options: insertOptions, callback: callback) wait(for: [expectation], timeout: 20.0) @@ -479,7 +479,7 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(tokens: true, additionalFields: additionalFields)) + container?.collect(callback: callback, options: CollectOptions(additionalFields: additionalFields)) wait(for: [expectation], timeout: 20.0) diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 5cdbf3f5..0a132c11 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -60,7 +60,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -74,7 +74,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -92,7 +92,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -112,7 +112,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -131,7 +131,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -151,7 +151,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Pure insert call") let callback = DemoAPICallback(expectation: expectation) - skyflow.insert(records: payload, options: InsertOptions(tokens: true), callback: callback) + skyflow.insert(records: payload, options: InsertOptions(), callback: callback) wait(for: [expectation], timeout: 10.0) let responseData = callback.receivedResponse.utf8 @@ -400,7 +400,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { let additionalFields: [String: Any] = [ "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"] + ["table": "table1", "fields": ["column1": "value1"], "skyflowId": "id1"] ] ] let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) @@ -417,7 +417,7 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowID: "id1") + let collectInput1 = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "id1") let cardNumber = container?.create(input: collectInput1, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) @@ -443,4 +443,29 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: .INSERT)).localizedDescription) } + func testCollectCallbackOnFailureParsesStructuredAPIError() { + let wholeRequestFailure: [String: Any] = [ + "error": [ + "grpcCode": 13, + "httpCode": 500, + "message": "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.", + "httpStatus": "Internal Server Error", + "details": [] + ] + ] + var receivedError: SkyflowError? + let collectCallback = CollectCallback( + onSuccess: { _ in XCTFail("onSuccess should not be called") }, + onFailure: { error in receivedError = error } + ) + + collectCallback.onFailure(wholeRequestFailure) + + XCTAssertEqual(receivedError?.httpCode, 500) + XCTAssertEqual(receivedError?.message, "Skyflow services experienced an internal error. Contact Skyflow support with request ID 2db2c594-b9a5-48db-a220-f936e12a43e9 for more information.") + XCTAssertEqual(receivedError?.grpcCode, 13) + XCTAssertEqual(receivedError?.httpStatus, "Internal Server Error") + XCTAssertEqual(receivedError?.details?.count, 0) + } + } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index bc3e5696..030c08d9 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -14,7 +14,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { self.collectCallback = FlowVaultCollectAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: FlowVaultICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } @@ -245,7 +245,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) @@ -282,7 +282,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) @@ -319,7 +319,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) @@ -360,7 +360,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": insertRecords], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) @@ -408,7 +408,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { """ let responseData = json.data(using: .utf8)! let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 200, httpVersion: "1.1", headerFields: nil) - self.collectCallback.options = FlowVaultICOptions(tokens: true) + self.collectCallback.options = FlowVaultICOptions() do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) @@ -484,7 +484,7 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { """ let responseData = json.data(using: .utf8)! let urlResponse = HTTPURLResponse(url: URL(string: "https://example.org/v2/records/update")!, statusCode: 207, httpVersion: "1.1", headerFields: nil) - self.collectCallback.options = FlowVaultICOptions(tokens: true) + self.collectCallback.options = FlowVaultICOptions() do { let processed = try self.collectCallback.processResponse(data: responseData, response: urlResponse, error: nil) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift index 2b7612f5..5f119654 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_dormantCollectRequestBodyTests.swift @@ -39,7 +39,7 @@ final class skyflow_iOS_dormantCollectRequestBodyTests: XCTestCase { func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { let additionalFields: [String: Any] = [ "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowID": "id1"] + ["table": "table1", "fields": ["column1": "value1"], "skyflowId": "id1"] ] ] let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) @@ -58,7 +58,7 @@ final class skyflow_iOS_dormantCollectRequestBodyTests: XCTestCase { let container = skyflow.container(type: ContainerType.COLLECT, options: nil) let options = CollectElementOptions(required: false) - let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER, skyflowID: "id1") + let cardNumberInput = CollectElementInput(table: "persons", column: "card_number", type: .CARD_NUMBER, skyflowId: "id1") let cardNumber = container?.create(input: cardNumberInput, options: options) cardNumber?.textField.secureText = "4111 1111 1111 1111" window.addSubview(cardNumber!) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift index 9368f46f..8bab8c4f 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_insertUtilTests 2.swift @@ -14,7 +14,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { self.collectCallback = FlowVaultInsertAPICallback(callback: DemoAPICallback(expectation: XCTestExpectation()), apiClient: APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()), records: defaultRecord, - options: FlowVaultICOptions(tokens: false, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions()) } @@ -238,7 +238,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) @@ -275,7 +275,7 @@ final class skyflow_iOS_insertUtilTests: XCTestCase { callback: callback, apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), records: ["records": [insertRecord]], - options: FlowVaultICOptions(tokens: true, additionalFields: nil), + options: FlowVaultICOptions(additionalFields: nil), contextOptions: ContextOptions() ) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index d81ff4d8..eba4ef05 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -305,9 +305,10 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { wait(for: [self.expectation], timeout: 20.0) waitForUIUpdates() - let errors = self.callback.data["records"] as! [[String: Any]] - XCTAssertEqual(errors.count, 1) - XCTAssertEqual((errors[0]["error"] as? NSError)?.code, -1009) + // Not scoped to any specific token, so it's delivered via onFailure (not folded into the + // records array) rather than a per-record error. + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "The Internet connection appears to be offline.") // No token to match this error against, so the element shows no inline error. XCTAssertEqual(element1.errorMessage.text, nil) XCTAssertEqual(element1.actualValue, nil) @@ -646,9 +647,9 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { container?.reveal(callback: callback.asRevealCallback) wait(for: [expectation], timeout: 20.0) - print(callback.data) - let errors = callback.data["records"] as! [[String: NSError]] - XCTAssertTrue(errors[0]["error"]!.localizedDescription.contains("Token generated from 'getBearerToken' callback function is invalid")) + // Invalid bearer token isn't scoped to any specific token, so RevealValueCallback routes it + // through onFailure as a Skyflow.SkyflowError rather than folding it into the records array. + XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index 3df0a7e7..ce25a027 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -62,7 +62,7 @@ final class skyflow_iOS_utilTests: XCTestCase { XCTAssertEqual(textField.secureText, "4111 1111 1111 1111") } func testConstructV2RequestBody() { - let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(tokens: true)) + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions()) XCTAssertEqual(result["vaultID"] as! String, "vault123") let records = result["records"] as! [[String: Any]] XCTAssertEqual(records[0]["tableName"] as! String, "table") @@ -71,7 +71,7 @@ final class skyflow_iOS_utilTests: XCTestCase { func testConstructV2RequestBodyWithUpsert() { let upsert = [UpsertOption(table: "table", uniqueColumns: ["field1"], updateType: .REPLACE)] - let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(tokens: true, upsert: upsert)) + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) let records = result["records"] as! [[String: Any]] let upsertPayload = records[0]["upsert"] as! [String: Any] XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) From 03ed970ee2a9b7e4030bc579bf751b62412a6c26 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 29 Jul 2026 00:44:36 +0530 Subject: [PATCH 10/14] SK-3005 update type for additional fields --- README.md | 39 ++- .../ComposableElements/ViewController.swift | 8 +- .../Skyflow/collect/AdditionalFields.swift | 29 ++ .../Skyflow/collect/CollectContainer.swift | 42 +-- Sources/Skyflow/collect/CollectOptions.swift | 4 +- .../collect/FlowVaultCollectRequestBody.swift | 18 +- .../Skyflow/collect/FlowVaultICOptions.swift | 4 +- .../collect/FlowVaultInsertAPICallback.swift | 8 +- .../composable/ComposableContainer.swift | 53 +--- Sources/Skyflow/errors/ErrorCodes.swift | 25 +- .../skyflow_iOS_collectTests.swift | 108 +------ ...skyflow_iOS_composableEelementsTests.swift | 12 - .../Skyflow_iOS_collectErrorTests.swift | 270 +++++++++++++++--- .../Skyflow_iOS_generalErrorTests.swift | 57 +++- .../MockURLProtocol.swift | 41 +++ .../skyflow_iOS_collectUtilTests.swift | 144 ++++++++++ .../skyflow_iOS_revealUtilTests.swift | 75 ++++- 17 files changed, 675 insertions(+), 262 deletions(-) create mode 100644 Sources/Skyflow/collect/AdditionalFields.swift create mode 100644 Tests/skyflow-iOS-utilTests/MockURLProtocol.swift diff --git a/README.md b/README.md index 6d5c9b85..106bced3 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,9 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { When you submit the form, call the `collect(callback: Skyflow.CollectCallback, options: Skyflow.CollectOptions? = nil)` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) @@ -446,7 +448,9 @@ let skyflowElement = container?.create(input: input, options: requiredOption) // Can interact with this object as a normal UIView Object and add to View // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] @@ -587,12 +591,14 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { ### Step 4 : Update data from Elements When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes a object of optional parameters as shown below: -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to update or insert into the vault alongside whatever's collected from the mounted elements. - `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE", "skyflowId": "value"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"], skyflowId: "value") +]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) @@ -654,7 +660,10 @@ let skyflowElement = container?.create(input: input, options: requiredOption) // Can interact with this object as a normal UIView Object and add to View // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"]], ["table": "cards", "fields": ["first_name": "Joe"], "skyflowId": "431eaa6c-5c15-4513-aa15-29f50babe882"]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]), + Skyflow.AdditionalFieldsRecord(table: "cards", fields: ["first_name": "Joe"], skyflowId: "431eaa6c-5c15-4513-aa15-29f50babe882") +]) //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] @@ -1169,12 +1178,14 @@ When you submit the form, call the `collect(options: Skyflow.CollectOptions? = n The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `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. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. - `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) @@ -1265,7 +1276,9 @@ do { } // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] @@ -1633,12 +1646,14 @@ When you submit the form, call the `collect(options: Skyflow.CollectOptions? = n The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `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. +- `additionalFields`: A `Skyflow.AdditionalFields` object - non-PCI records to insert into the vault alongside whatever's collected from the mounted elements. - `upsert`: An array of `Skyflow.UpsertOption` objects to support upsert while collecting data from Skyflow elements. Each option specifies the `table`, the `uniqueColumns` used to match existing records, and an optional `updateType` (`UpdateType.UPDATE` merges the new fields into the matched record, `UpdateType.REPLACE` replaces it). ```swift // Non-PCI records -let nonPCIRecords = ["table": "persons", "fields": [["gender": "MALE"]]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"]) +]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] // Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) @@ -1732,7 +1747,9 @@ do { } // Non-PCI records -let nonPCIRecords = [["table": "persons", "fields": ["gender": "MALE"],"skyflowId": "77dc3caf-c452-49e1-8625-07219d7567bf"]] +let nonPCIRecords = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "persons", fields: ["gender": "MALE"], skyflowId: "77dc3caf-c452-49e1-8625-07219d7567bf") +]) //Upsert options let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index 5e863434..27fbf016 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -188,11 +188,9 @@ class ViewController: UIViewController { // additionalFields: extra records submitted alongside whatever's collected from // the mounted elements, one entry per table. - let additionalFields: [String: Any] = [ - "records": [ - ["table": "credit_cards", "fields": ["billing_zip": "94105"]] - ] - ] + let additionalFields = Skyflow.AdditionalFields(records: [ + Skyflow.AdditionalFieldsRecord(table: "credit_cards", fields: ["billing_zip": "94105"]) + ]) container!.collect(callback: collectCallback, options: Skyflow.CollectOptions(additionalFields: additionalFields, upsert: upsertOptions)) } diff --git a/Sources/Skyflow/collect/AdditionalFields.swift b/Sources/Skyflow/collect/AdditionalFields.swift new file mode 100644 index 00000000..62fd0c99 --- /dev/null +++ b/Sources/Skyflow/collect/AdditionalFields.swift @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Typed replacement for the old [String: Any] additionalFields shape on CollectOptions - +// non-PCI records to insert/update alongside whatever's collected from mounted elements. + +import Foundation + +public struct AdditionalFieldsRecord { + public let table: String + public let fields: [String: Any] + // Present to update an existing record, absent to insert a new one. + public let skyflowId: String? + + public init(table: String, fields: [String: Any], skyflowId: String? = nil) { + self.table = table + self.fields = fields + self.skyflowId = skyflowId + } +} + +public struct AdditionalFields { + public let records: [AdditionalFieldsRecord] + + public init(records: [AdditionalFieldsRecord]) { + self.records = records + } +} diff --git a/Sources/Skyflow/collect/CollectContainer.swift b/Sources/Skyflow/collect/CollectContainer.swift index 555003b8..2a5b1d91 100644 --- a/Sources/Skyflow/collect/CollectContainer.swift +++ b/Sources/Skyflow/collect/CollectContainer.swift @@ -62,26 +62,16 @@ public extension Container { callback.onFailure(SkyflowError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: errors])) return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkRecord(record: record, index: index) + if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return } } let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) @@ -117,27 +107,13 @@ public extension Container { return nil } - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { + private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.table.isEmpty { return .EMPTY_TABLE_NAME() } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ + if record.fields.isEmpty { return .EMPTY_FIELDS_KEY(value: "\(index)") } - return nil } } diff --git a/Sources/Skyflow/collect/CollectOptions.swift b/Sources/Skyflow/collect/CollectOptions.swift index 2f4bbbff..922b1f44 100644 --- a/Sources/Skyflow/collect/CollectOptions.swift +++ b/Sources/Skyflow/collect/CollectOptions.swift @@ -7,9 +7,9 @@ import Foundation public struct CollectOptions { - var additionalFields: [String: Any]? + var additionalFields: AdditionalFields? var upsert: [UpsertOption]? - public init(additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil) { + public init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil) { self.additionalFields = additionalFields self.upsert = upsert } diff --git a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift index 2e48cbd3..aa53482f 100644 --- a/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift +++ b/Sources/Skyflow/collect/FlowVaultCollectRequestBody.swift @@ -54,7 +54,7 @@ internal class FlowVaultCollectRequestBody { internal static func createRequestBody( elements: [TextField], - additionalFields: [String: Any]? = nil, + additionalFields: AdditionalFields? = nil, callback: Callback, contextOptions: ContextOptions ) -> [String: Any]? { @@ -65,16 +65,14 @@ internal class FlowVaultCollectRequestBody { self.breakFlag = false self.tableSet = Set() var index: Int = 0 - var inputPayload: [[String: Any]] = [] - if additionalFields != nil { - inputPayload = additionalFields?["records"] as! [[String: Any]] - for entry in inputPayload { - let entryDict = entry - let tableName = entryDict["table"] as! String - let fields = entryDict["fields"] as! [String: Any] - if entryDict["skyflowId"] != nil { - let skyflowId = entryDict["skyflowId"] as! String + if let additionalFields = additionalFields { + for entry in additionalFields.records { + let tableName = entry.table + let fields = entry.fields + // Matches the element-based check below: an empty string is treated the same as + // absent, falling back to a plain insert rather than an update targeting "". + if let skyflowId = entry.skyflowId, !skyflowId.isEmpty { if updatePayload[skyflowId] != nil { let temp = updatePayload[skyflowId] as! [String: Any] var existingFields = temp["fields"] as! [String: Any] diff --git a/Sources/Skyflow/collect/FlowVaultICOptions.swift b/Sources/Skyflow/collect/FlowVaultICOptions.swift index e36e1032..b4ba2360 100644 --- a/Sources/Skyflow/collect/FlowVaultICOptions.swift +++ b/Sources/Skyflow/collect/FlowVaultICOptions.swift @@ -7,12 +7,12 @@ import Foundation internal struct FlowVaultICOptions { - var additionalFields: [String: Any]? + var additionalFields: AdditionalFields? var upsert: [UpsertOption]? var callback: Callback? var contextOptions: ContextOptions? - init(additionalFields: [String: Any]? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { + init(additionalFields: AdditionalFields? = nil, upsert: [UpsertOption]? = nil, callback: Callback? = nil, contextOptions: ContextOptions? = nil) { self.additionalFields = additionalFields self.upsert = upsert self.callback = callback diff --git a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift index bae16313..22a99ac2 100644 --- a/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift +++ b/Sources/Skyflow/collect/FlowVaultInsertAPICallback.swift @@ -8,6 +8,10 @@ import Foundation import UIKit internal class FlowVaultInsertAPICallback: Callback { + // Overridable only for tests (e.g. injecting a URLProtocol mock via protocolClasses) - global + // URLProtocol.registerClass(_:) isn't reliably consulted for manually-created URLSession + // instances in every environment, so tests need a real seam here instead. + internal static var urlSessionConfiguration: URLSessionConfiguration = .default var apiClient: APIClient var records: [String: Any] var callback: Callback @@ -177,7 +181,7 @@ internal class FlowVaultInsertAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - return (request, URLSession(configuration: .default)) + return (request, URLSession(configuration: Self.urlSessionConfiguration)) } @@ -204,7 +208,7 @@ internal class FlowVaultInsertAPICallback: Callback { request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(jsonString, forHTTPHeaderField: "sky-metadata") - return (request, URLSession(configuration: .default)) + return (request, URLSession(configuration: Self.urlSessionConfiguration)) } func processResponse(data: Data?, response: URLResponse?, error: Error?) throws -> [String: Any] { diff --git a/Sources/Skyflow/composable/ComposableContainer.swift b/Sources/Skyflow/composable/ComposableContainer.swift index e5e35f66..68f1510b 100644 --- a/Sources/Skyflow/composable/ComposableContainer.swift +++ b/Sources/Skyflow/composable/ComposableContainer.swift @@ -246,26 +246,16 @@ public extension Container { return } - if options?.additionalFields != nil { - if options?.additionalFields!["records"] == nil { - errorCode = .MISSING_RECORDS_IN_ADDITIONAL_FIELDS() + if let additionalFields = options?.additionalFields { + if additionalFields.records.isEmpty { + errorCode = .EMPTY_RECORDS_OBJECT() return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - if let additionalFieldEntries = options?.additionalFields!["records"] as? [[String: Any]] { - if additionalFieldEntries.isEmpty { - errorCode = .EMPTY_RECORDS_OBJECT() + for (index, record) in additionalFields.records.enumerated() { + errorCode = checkRecord(record: record, index: index) + if errorCode != nil { return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) } - for (index, record) in additionalFieldEntries.enumerated() { - errorCode = checkRecord(record: record, index: index) - if errorCode != nil { - return callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - } - } - } else { - errorCode = .INVALID_RECORDS_TYPE() - callback.onFailure(errorCode!.getErrorObject(contextOptions: tempContextOptions)) - return } } let records = FlowVaultCollectRequestBody.createRequestBody(elements: self.elements, additionalFields: options?.additionalFields, callback: callback, contextOptions: tempContextOptions) @@ -301,29 +291,14 @@ public extension Container { return nil } - private func checkRecord(record: [String: Any], index: Int) -> ErrorCodes? { - if record["table"] == nil { - return .TABLE_KEY_ERROR(value: "\(index)") - } - if !(record["table"] is String) { - return .INVALID_TABLE_NAME_TYPE(value: "\(index)") - } - if (record["table"] as? String == "") { - return .EMPTY_TABLE_NAME() - } - if record["fields"] == nil { - return .FIELDS_KEY_ERROR(value: "\(index)") - } - if !(record["fields"] is [String: Any]) { - return .INVALID_FIELDS_TYPE(value: "\(index)") - } - let fields = record["fields"] as! [String: Any] - if (fields.isEmpty){ - return .EMPTY_FIELDS_KEY(value: "\(index)") - } - - return nil + private func checkRecord(record: AdditionalFieldsRecord, index: Int) -> ErrorCodes? { + if record.table.isEmpty { + return .EMPTY_TABLE_NAME() } - + if record.fields.isEmpty { + return .EMPTY_FIELDS_KEY(value: "\(index)") + } + return nil + } } diff --git a/Sources/Skyflow/errors/ErrorCodes.swift b/Sources/Skyflow/errors/ErrorCodes.swift index d3c162a5..cd489d73 100644 --- a/Sources/Skyflow/errors/ErrorCodes.swift +++ b/Sources/Skyflow/errors/ErrorCodes.swift @@ -198,14 +198,25 @@ public class SkyflowError: NSError { // Recover the underlying NSError instead of stringifying the whole container dict. // (Container.reveal()'s equivalent failures are already unwrapped earlier, // by RevealValueCallback.onFailure.) + // + // A third shape reaches here too: FlowVaultInsertAPICallback's insert+update merge + // (group.notify in onSuccess) builds mergedErrors entries as {"error": {"message", + // "httpCode", ...}} - a JSON-decoded API error detail dict, not an NSError - so it's + // handled by trying SkyflowError(apiError:) on the entry itself, which expects exactly + // that {"error": {...}} shape. if let dict = error as? [String: Any], let errorsArray = dict["errors"] as? [Any] { - let firstError = errorsArray.compactMap { entry -> NSError? in - if let nsError = entry as? NSError { return nsError } - if let nested = entry as? [String: Any] { return nested["error"] as? NSError } - return nil - }.first - if let firstError = firstError { - return SkyflowError(domain: firstError.domain, code: firstError.code, userInfo: firstError.userInfo) + for entry in errorsArray { + if let nsError = entry as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let nested = entry as? [String: Any] { + if let nsError = nested["error"] as? NSError { + return SkyflowError(domain: nsError.domain, code: nsError.code, userInfo: nsError.userInfo) + } + if let apiError = SkyflowError(apiError: entry) { + return apiError + } + } } } return SkyflowError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error)"]) diff --git a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift index 58117c95..152bedd6 100644 --- a/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift +++ b/Tests/skyflow-iOS-collectTests/skyflow_iOS_collectTests.swift @@ -743,122 +743,42 @@ final class skyflow_iOS_collectTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_VAULT_URL().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - func testCollectBadTypeAddionalFields() { - let additionalFields = ["records": "records"] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectNoRecordsInAddionalFields() { - let additionalFields = ["typo": []] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, - ErrorCodes.MISSING_RECORDS_IN_ADDITIONAL_FIELDS() - .getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - func testCollectEmptyRecordsAddionalFields() { - let additionalFields = ["records": []] + let additionalFields = AdditionalFields(records: []) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_RECORDS_OBJECT().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoTableKeyAddionalFields() { - let additionalFields = ["records": [[:]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyTableAddionalFields() { - let additionalFields = ["records": [["table": ""]]] + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(table: "", fields: ["field": "value"])]) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_TABLE_NAME().getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } - - func testCollectNoFieldsKeyAddionalFields() { - let additionalFields = ["records": [["table": "table"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.FIELDS_KEY_ERROR(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - - func testCollectInvalidFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": "fields"]]] - let container = skyflow.container(type: ContainerType.COLLECT) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_FIELDS_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) - } - + func testCollectEmptyFieldsAddionalFields() { - let additionalFields = ["records": [["table": "table", "fields": [:]]]] + let additionalFields = AdditionalFields(records: [AdditionalFieldsRecord(table: "table", fields: [:])]) let container = skyflow.container(type: ContainerType.COLLECT) - + let expectation = XCTestExpectation() let callback = DemoAPICallback(expectation: expectation) container?.collect(callback: callback.asCollectCallback, options: CollectOptions(additionalFields: additionalFields)) - + wait(for: [expectation], timeout: 20.0) - + XCTAssertEqual(callback.receivedResponse, ErrorCodes.EMPTY_FIELDS_KEY(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COLLECT_CONTAINER)).localizedDescription) } diff --git a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift index 8fb2d3bb..19d80603 100644 --- a/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift +++ b/Tests/skyflow-iOS-composableTests/skyflow_iOS_composableEelementsTests.swift @@ -473,18 +473,6 @@ final class skyflow_iOS_composableEelementsTests: XCTestCase { XCTAssertEqual(callback.receivedResponse, ErrorCodes.TABLE_NAME_IS_EMPTY_FOR_ATLEAST_ONE_UPSERT_OPTION(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.INSERT)).localizedDescription) } - func testCollectBadTableKeyAddionalFields() { - let additionalFields = ["records": [["table": []]]] - let container = skyflow.container(type: ContainerType.COMPOSABLE) - - let expectation = XCTestExpectation() - let callback = DemoAPICallback(expectation: expectation) - container?.collect(callback: callback, options: CollectOptions(additionalFields: additionalFields)) - - wait(for: [expectation], timeout: 20.0) - - XCTAssertEqual(callback.receivedResponse, ErrorCodes.INVALID_TABLE_NAME_TYPE(value: "0").getErrorObject(contextOptions: ContextOptions(interface: InterfaceName.COMPOSABLE_CONTAINER)).localizedDescription) - } func testComposableCollectEmptyVaultURL() { let clientWithEmptyURL = Client(Configuration(vaultID: "id", vaultURL: "", tokenProvider: DemoTokenProvider())) diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 0a132c11..8b480dcb 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -290,21 +290,16 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - "name": "John Doe" - ]] - ]] + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["cvv": "123", "name": "John Doe"]) + ]) FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyDuplicateInAdditionalFields() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -321,22 +316,13 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "duplicate": "123", - "name": "John Doe" - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123", "name": "John Doe"]), + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123"]) + ]) FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ADDITIONAL_FIELD_FOUND(value: "duplicate").description) } @@ -356,25 +342,17 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { let expectation = XCTestExpectation(description: "Container insert call - All valid") let callback = DemoAPICallback(expectation: expectation) - let fields: [String: Any] = [ - "records": [[ - "table": "persons", - "fields": [ - "cvv": "123", - ]], - [ - "table": "persons", - "fields": [ - "duplicate": "123", - ]] - ]] + let fields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["cvv": "123"]), + AdditionalFieldsRecord(table: "persons", fields: ["duplicate": "123"]) + ]) FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!, cvv!], additionalFields: fields,callback: callback, contextOptions: ContextOptions(interface: .COLLECT_CONTAINER)) wait(for: [expectation], timeout: 10.0) - + let responseData = callback.receivedResponse XCTAssertEqual(responseData, ErrorCodes.DUPLICATE_ELEMENT_FOUND(values: ["cvv", "persons"]).description) } - + func testCreateRequestBodyWithOnlyInserts() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -395,14 +373,36 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(requestBody?["records"] as! [NSDictionary], [ ["table": "persons", "fields": ["card_number": "", "cvv": ""]] ]) + // Every plain CollectElementInput defaults to skyflowId: "" (not nil) - confirms that + // default doesn't accidentally get treated as "update targeting an empty ID". + XCTAssertTrue((requestBody?["update"] as! [String: Any]).isEmpty) + } + + func testCreateRequestBodyExplicitEmptyStringSkyflowIdOnElementFallsBackToInsert() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + let collectInput = CollectElementInput(table: "persons", column: "card_number", placeholder: "card number", type: .CARD_NUMBER, skyflowId: "") + let cardNumber = container?.create(input: collectInput, options: options) + cardNumber?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(cardNumber!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Explicit empty-string skyflowId falls back to insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [cardNumber!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "persons") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) } func testCreateRequestBodyWithSkyflowIDInAdditionalFields() { - let additionalFields: [String: Any] = [ - "records": [ - ["table": "table1", "fields": ["column1": "value1"], "skyflowId": "id1"] - ] - ] + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "table1", fields: ["column1": "value1"], skyflowId: "id1") + ]) let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Update via additionalFields")) let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) XCTAssertNotNil(requestBody) @@ -413,6 +413,44 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(entry["fields"] as! [String: String], ["column1": "value1"]) } + func testCreateRequestBodyEmptyStringSkyflowIdOnAdditionalFieldsFallsBackToInsert() { + // Matches element-based skyflowId handling: an empty string is treated the same as + // absent (falls back to a plain insert), rather than creating an update targeting "". + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "table1", fields: ["column1": "value1"], skyflowId: "") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Empty skyflowId falls back to insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "table1") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyMergesMultipleAdditionalFieldsSharingSkyflowId() { + // Two additionalFields entries that both target the same skyflowId should merge their + // fields into a single update payload entry, not clobber or duplicate it. + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "persons", fields: ["name": "John"], skyflowId: "id1"), + AdditionalFieldsRecord(table: "persons", fields: ["email": "john@example.com"], skyflowId: "id1") + ]) + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Merge additionalFields sharing a skyflowId")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + XCTAssertEqual(entry["table"] as! String, "persons") + let fields = entry["fields"] as! [String: String] + XCTAssertEqual(fields["name"], "John") + XCTAssertEqual(fields["email"], "john@example.com") + } + func testCreateRequestBodyWithSkyflowIDOnElement() { let window = UIWindow() let container = skyflow.container(type: ContainerType.COLLECT, options: nil) @@ -432,6 +470,152 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(entry["fields"] as! [String: String], ["card_number": ""]) } + func testCreateRequestBodyMixedInsertAndUpdateElements() { + // One plain element (no skyflowId) and one update-by-skyflowId element in the same + // collect() call - the plain one should land in "records", the tagged one in "update", + // without either bucket interfering with the other. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Mixed insert and update elements")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "cards") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let updateEntry = update["id1"] as! [String: Any] + XCTAssertEqual(updateEntry["table"] as! String, "persons") + } + + func testCreateRequestBodyPlainInsertViaAdditionalFieldsCombinedWithElement() { + // additionalFields without a skyflowId is a plain insert, same as a mounted element - + // both should end up in "records" together, none in "update". + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]) + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element insert + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertTrue(update.isEmpty) + } + + func testCreateRequestBodyElementUpdateWithAdditionalFieldsInsert() { + // An update-by-skyflowId element combined with a plain-insert additionalFields record - + // "records" should only contain the additionalFields insert, "update" only the element. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]) + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Element update + additionalFields insert")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 1) + XCTAssertEqual(records[0]["table"] as! String, "accounts") + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") + } + + func testFullCombinationCollectUpdateUpsertAndAdditionalFields() { + // The kitchen-sink scenario: an insert element, an update-by-skyflowId element, an + // insert additionalFields record, and an update additionalFields record, all in one + // request - then upsert applied on top. Upsert only ever touches FlowVaultInsertRequestBody's + // "records" bucket (confirmed: FlowVaultUpdateRequestBody never reads options.upsert at + // all), so it should only decorate the matching insert record and leave "update" untouched. + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let insertInput = CollectElementInput(table: "cards", column: "card_number", placeholder: "card number", type: .CARD_NUMBER) + let insertElement = container?.create(input: insertInput, options: options) + insertElement?.textField.secureText = "4111 1111 1111 1111" + window.addSubview(insertElement!) + + let updateInput = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement = container?.create(input: updateInput, options: options) + updateElement?.textField.secureText = "John Doe" + window.addSubview(updateElement!) + + let additionalFields = AdditionalFields(records: [ + AdditionalFieldsRecord(table: "accounts", fields: ["status": "active"]), + AdditionalFieldsRecord(table: "billing", fields: ["zip": "94105"], skyflowId: "id2") + ]) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Full combination")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [insertElement!, updateElement!], additionalFields: additionalFields, callback: callback, contextOptions: ContextOptions()) + XCTAssertNotNil(requestBody) + + let records = requestBody?["records"] as! [[String: Any]] + XCTAssertEqual(records.count, 2) + XCTAssertTrue(records.contains { $0["table"] as? String == "cards" }) + XCTAssertTrue(records.contains { $0["table"] as? String == "accounts" }) + + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 2) + XCTAssertEqual((update["id1"] as! [String: Any])["table"] as! String, "persons") + XCTAssertEqual((update["id2"] as! [String: Any])["table"] as! String, "billing") + + // Apply upsert on top, matching only the "cards" table. + let upsertOptions = [UpsertOption(table: "cards", uniqueColumns: ["card_number"], updateType: .UPDATE)] + let wireBody = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: requestBody!, options: FlowVaultICOptions(upsert: upsertOptions)) + let wireRecords = wireBody["records"] as! [[String: Any]] + + let cardsWireRecord = wireRecords.first { $0["tableName"] as? String == "cards" } + XCTAssertNotNil(cardsWireRecord?["upsert"]) + + let accountsWireRecord = wireRecords.first { $0["tableName"] as? String == "accounts" } + XCTAssertNil(accountsWireRecord?["upsert"]) + + // update bucket is a completely separate path (FlowVaultUpdateRequestBody) - upsert has + // no way to reach or affect it, confirmed structurally since FlowVaultInsertRequestBody + // only ever reads requestBody["records"]. + XCTAssertEqual(update.count, 2) + } + func testInsertEmptyVaultURL() { let expectation = XCTestExpectation(description: "Insert with empty vaultURL should fail") let callback = DemoAPICallback(expectation: expectation) diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift index faf14185..1c3e52e9 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_generalErrorTests.swift @@ -138,8 +138,63 @@ class Skyflow_iOS_generalErrorTests: XCTestCase { func testIsTokenValid() { let apiClient = APIClient(vaultID: "", vaultURL: "", tokenProvider: DemoTokenProvider()) let expectation = XCTestExpectation(description: "should get token") - + XCTAssertEqual(false, apiClient.isTokenValid()) } + + func testSkyflowErrorWrapReturnsExistingInstanceUnchanged() { + let original = SkyflowError(domain: "TestDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "original message"]) + let wrapped = SkyflowError.wrap(original) + XCTAssertTrue(wrapped === original) + } + + func testSkyflowErrorWrapPreservesPlainNSError() { + let nsError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "The Internet connection appears to be offline."]) + let wrapped = SkyflowError.wrap(nsError) + + XCTAssertEqual(wrapped.domain, "NSURLErrorDomain") + XCTAssertEqual(wrapped.httpCode, -1009) + XCTAssertEqual(wrapped.message, "The Internet connection appears to be offline.") + XCTAssertNil(wrapped.grpcCode) + XCTAssertNil(wrapped.httpStatus) + XCTAssertNil(wrapped.details) + } + + func testSkyflowErrorWrapFallsBackForOpaqueValue() { + // Anything that isn't a SkyflowError, the structured {"error": {...}} API shape, an + // NSError, or the internal {"errors": [...]} wrapping still needs to produce *something* + // usable rather than crash. + let wrapped = SkyflowError.wrap("just a plain string, not an error at all") + XCTAssertEqual(wrapped.httpCode, 0) + XCTAssertTrue(wrapped.message.contains("just a plain string")) + } + + func testSkyflowAPIErrorInitMissingOptionalFields() { + let apiError: [String: Any] = [ + "error": ["httpCode": 500, "message": "Internal error"] + ] + let skyflowError = SkyflowError(apiError: apiError) + + XCTAssertEqual(skyflowError?.httpCode, 500) + XCTAssertEqual(skyflowError?.message, "Internal error") + XCTAssertNil(skyflowError?.grpcCode) + XCTAssertNil(skyflowError?.httpStatus) + XCTAssertNil(skyflowError?.details) + } + + func testSkyflowAPIErrorInitReturnsNilForMalformedInput() { + XCTAssertNil(SkyflowError(apiError: "not a dictionary")) + XCTAssertNil(SkyflowError(apiError: ["message": "no nested error key"])) + } + + func testSkyflowErrorWrapRecoversNSErrorFromNestedErrorsArray() { + // Mirrors what FlowVaultRevealAPICallback.callRevealOnFailure produces for a + // whole-request failure reaching Client.detokenize() directly (no RevealValueCallback + // in between to unwrap it first). + let underlying = NSError(domain: "", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid bearer token"]) + let wrapped = SkyflowError.wrap(["errors": [["error": underlying]]]) + XCTAssertEqual(wrapped.httpCode, 400) + XCTAssertEqual(wrapped.message, "Invalid bearer token") + } } diff --git a/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift new file mode 100644 index 00000000..64a125f9 --- /dev/null +++ b/Tests/skyflow-iOS-utilTests/MockURLProtocol.swift @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 Skyflow +*/ + +// Minimal URLProtocol-based network mock. Register via URLProtocol.registerClass(MockURLProtocol.self) +// before the request fires (URLSession(configuration: .default) consults globally registered +// protocol classes), and unregister afterward. Used for tests that need to exercise real +// URLSession.dataTask call sites (e.g. FlowVaultInsertAPICallback's insert+update merge), where +// bypassing the network layer entirely (as most other tests do via processResponse(data:response:error:)) +// wouldn't exercise the merge logic itself. + +import Foundation + +final class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "MockURLProtocol", code: -1, userInfo: [NSLocalizedDescriptionKey: "No requestHandler configured"])) + return + } + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift index 030c08d9..7f3662e3 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_collectUtilTests.swift @@ -497,4 +497,148 @@ final class skyflow_iOS_collectUtilTests: XCTestCase { XCTFail("Update partial response should not throw: \(error)") } } + + // The tests above all call processResponse(data:response:error:) directly, which only + // exercises parsing a single HTTP response body. When a request has BOTH inserts and + // updates, FlowVaultInsertAPICallback.onSuccess fires two REAL network calls (v2/records/insert + // and v2/records/update) and merges both results via a DispatchGroup - that merge logic can + // only be exercised by going through onSuccess with real URLSession dispatch, hence the + // URLProtocol mock here. + func testInsertAndUpdateResponsesMergeIntoOneCollectResponse() { + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + let body: [String: Any] + if url.contains("v2/records/insert") { + body = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + } else if url.contains("v2/records/update") { + body = ["records": [["skyflowID": "id1", "tableName": "persons", "tokens": [:], "httpCode": 200]]] + } else { + XCTFail("Unexpected URL: \(url)") + body = ["records": []] + } + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } + + let expectation = XCTestExpectation(description: "Merged insert+update response") + let callback = DemoAPICallback(expectation: expectation) + + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] + ] + + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: records, + options: FlowVaultICOptions(), + contextOptions: ContextOptions() + ) + // onSuccess's parameter here is the bearer token from TokenAPICallback - unused by this + // method's own logic (it reads self.records instead), so any value triggers the real flow. + insertApiCallback.onSuccess("dummy-token") + + wait(for: [expectation], timeout: 10.0) + + guard let collectResponse = callback.collectResponse else { + XCTFail("Expected a merged CollectResponse, got: receivedResponse=\(callback.receivedResponse) data=\(callback.data)") + return + } + XCTAssertEqual(collectResponse.records.count, 2) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "cards" && $0.skyflowID == "insertedId" }) + XCTAssertTrue(collectResponse.records.contains { $0.tableName == "persons" && $0.skyflowID == "id1" }) + } + + func testInsertSucceedsButUpdateFailsSurfacesAsSkyflowError() { + // One side of the merge fails outright (not a per-record error, a whole-side failure) - + // the merge should surface a proper Skyflow.SkyflowError with the real message, not a + // stringified dump of the internal {"records", "errors"} merge container. + let mockConfiguration = URLSessionConfiguration.ephemeral + mockConfiguration.protocolClasses = [MockURLProtocol.self] + let originalConfiguration = FlowVaultInsertAPICallback.urlSessionConfiguration + FlowVaultInsertAPICallback.urlSessionConfiguration = mockConfiguration + defer { FlowVaultInsertAPICallback.urlSessionConfiguration = originalConfiguration } + + MockURLProtocol.requestHandler = { request in + let url = request.url!.absoluteString + if url.contains("v2/records/insert") { + let body: [String: Any] = ["records": [["skyflowID": "insertedId", "tableName": "cards", "tokens": ["card_number": "value"], "httpCode": 200]]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } else if url.contains("v2/records/update") { + let body: [String: Any] = ["error": ["message": "Update service unavailable", "httpCode": 503]] + let data = try! JSONSerialization.data(withJSONObject: body) + let response = HTTPURLResponse(url: request.url!, statusCode: 503, httpVersion: "1.1", headerFields: nil)! + return (response, data) + } + XCTFail("Unexpected URL: \(url)") + return (HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: "1.1", headerFields: nil)!, Data()) + } + + let expectation = XCTestExpectation(description: "Update-side failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + let records: [String: Any] = [ + "records": [["table": "cards", "fields": ["card_number": "4111"]]], + "update": ["id1": ["table": "persons", "fields": ["name": "John"]]] + ] + + let insertApiCallback = FlowVaultInsertAPICallback( + callback: callback.asCollectCallback, + apiClient: APIClient(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: DemoTokenProvider()), + records: records, + options: FlowVaultICOptions(), + contextOptions: ContextOptions() + ) + insertApiCallback.onSuccess("dummy-token") + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(callback.receivedResponse, "Update service unavailable") + } + + func testCollectRecordHttpCodeDefaultsToZeroWhenMissing() { + // httpCode is declared non-optional (Int, not Int?) - verify a dict lacking the key + // doesn't crash and just defaults to 0 instead. + let record = CollectRecord(["tableName": "persons", "skyflowID": "id1"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + XCTAssertEqual(record.tableName, "persons") + } + + func testCollectRecordErrorDiscriminatesSuccessFromFailure() { + let success = CollectRecord(["tableName": "persons", "httpCode": 200]) + let failure = CollectRecord(["error": "Invalid request", "httpCode": 400]) + + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Invalid request") + XCTAssertEqual(failure.httpCode, 400) + } + + func testCollectResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(CollectResponse("not a dictionary")) + XCTAssertNil(CollectResponse(["typo": []])) + XCTAssertNil(CollectResponse(["records": "not an array"])) + } + + func testCollectResponseInitParsesValidBody() { + let response = CollectResponse([ + "records": [ + ["tableName": "persons", "skyflowID": "id1", "httpCode": 200], + ["error": "failed", "httpCode": 400] + ] + ]) + XCTAssertEqual(response?.records.count, 2) + XCTAssertNil(response?.records[0].error) + XCTAssertEqual(response?.records[1].error, "failed") + } } diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index eba4ef05..a24d7d22 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -651,5 +651,78 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { // through onFailure as a Skyflow.SkyflowError rather than folding it into the records array. XCTAssertTrue(callback.receivedResponse.contains("Token generated from 'getBearerToken' callback function is invalid")) } - + + func testRevealValueOnFailureMixedTokenScopedAndUnscopedErrorsRoutesToOnFailure() { + // Edge case: if a genuine network/API-level error (no "token") arrives in the same batch + // as per-token errors, the whole thing is treated as a whole-request failure - the + // per-token entries are discarded rather than partially surfaced via onSuccess. This + // documents that behavior explicitly, since it's not obvious from reading either branch + // in isolation. + let scopedToken = "123" + let networkError = NSError(domain: "NSURLErrorDomain", code: -1009, userInfo: [NSLocalizedDescriptionKey: "network down"]) + let response: [String: Any] = [ + "errors": [ + ["token": scopedToken, "error": "Invalid Token"], + ["error": networkError] + ] + ] + + let element1 = self.container.create(input: RevealElementInput(token: scopedToken, label: "first")) + self.revealValueCallback.revealElements = [element1] + + self.revealValueCallback.onFailure(response) + wait(for: [self.expectation], timeout: 20.0) + waitForUIUpdates() + + XCTAssertNil(self.callback.data["records"]) + XCTAssertEqual(self.callback.receivedResponse, "network down") + XCTAssertEqual(element1.errorMessage.text, nil) + } + + func testRevealRecordHttpCodeDefaultsToZeroWhenMissing() { + let record = RevealRecord(["token": "abc"]) + XCTAssertEqual(record.httpCode, 0) + XCTAssertNil(record.error) + } + + func testRevealRecordErrorDiscriminatesSuccessFromFailure() { + let success = RevealRecord(["token": "abc", "value": "1234", "httpCode": 200]) + let failure = RevealRecord(["token": "xyz", "error": "Tokens not found", "httpCode": 404]) + + XCTAssertNil(success.error) + XCTAssertEqual(failure.error, "Tokens not found") + XCTAssertEqual(failure.httpCode, 404) + } + + func testRevealResponseInitReturnsNilForMalformedBody() { + XCTAssertNil(RevealResponse("not a dictionary")) + XCTAssertNil(RevealResponse(["typo": []])) + XCTAssertNil(RevealResponse(["records": "not an array"])) + } + + // Verifies the real end-to-end wiring for a whole-request failure through + // FlowVaultRevealAPICallback -> callRevealOnFailure -> LogCallback -> RevealCallback -> + // SkyflowError.wrap, using a token provider that fails immediately (no network mocking + // needed - TokenAPICallback's failure reaches the exact same callRevealOnFailure wrapping + // as a real dispatch failure would). This is the plumbing that had a real bug (SkyflowError.wrap + // losing the message entirely) until it was found and fixed earlier via a unit test on + // SkyflowError.wrap directly - this test instead confirms the full real call chain, not just + // the isolated function. + func testDetokenizeTokenProviderFailureSurfacesAsSkyflowErrorThroughRealCallChain() { + class FailingTokenProvider: TokenProvider { + func getBearerToken(_ apiCallback: Callback) { + apiCallback.onFailure(NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "TokenProvider error"])) + } + } + let client = Client(Configuration(vaultID: "vault", vaultURL: "https://example.org/", tokenProvider: FailingTokenProvider())) + let expectation = XCTestExpectation(description: "TokenProvider failure surfaces as SkyflowError") + let callback = DemoAPICallback(expectation: expectation) + + client.detokenize(records: ["records": [["token": "tok1"]]], callback: callback.asRevealCallback) + + wait(for: [expectation], timeout: 10.0) + + XCTAssertEqual(callback.receivedResponse, "TokenProvider error") + } + } From 837f518d9e5eb40c363adbf612c0d2f482c3b15d Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 29 Jul 2026 16:52:08 +0530 Subject: [PATCH 11/14] SK-3005 update type for additional fields and redaction option in reveal options --- README.md | 31 ++++++++++++----- .../CollectAndRevealViewController.swift | 33 +++++++++++-------- .../ComposableElements/ViewController.swift | 33 +++++++++++-------- .../UpsertFeature/ViewController.swift | 23 +++++++++---- Sources/Skyflow/core/Client.swift | 2 +- .../FlowVaultDetokenizeRequestBody.swift | 12 ++----- Sources/Skyflow/reveal/RevealContainer.swift | 8 +---- .../Skyflow/reveal/RevealElementInput.swift | 6 +--- Sources/Skyflow/reveal/RevealOptions.swift | 6 +++- .../skyflow_iOS_revealTests.swift | 18 +++++----- .../skyflow_iOS_revealUtilTests.swift | 8 +++-- 11 files changed, 103 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 106bced3..c3155ced 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,8 @@ let collectElementInput = Skyflow.CollectElementInput( type: Skyflow.ElementType, // Skyflow.ElementType enum ) ``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note**: +The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. +**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() @@ -1858,13 +1859,11 @@ let revealElementInput = Skyflow.RevealElementInput( 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 it is not provided - redaction: String, // optional. Redaction to apply for the element's token group. E.g. "MASKED" - tokenGroupName: String, // optional. Name of the token group the token belongs to. ) ``` `Note`: - `token` is optional only if it is being used in invokeConnection() -- `redaction` and `tokenGroupName` work together: when both are provided, the redaction is applied to that token group as part of the detokenize call. If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. +- To apply a redaction to a token group as part of the detokenize call, use `tokenGroupRedactions` on `Skyflow.RevealOptions` (passed to `reveal(callback:options:)`) rather than on the individual `RevealElementInput` - see [Step 4: Reveal data](#step-4-reveal-data). If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. 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. @@ -1947,6 +1946,17 @@ let revealCallback = Skyflow.RevealCallback( ) container.reveal(callback: revealCallback) ``` + +To apply a redaction to one or more token groups as part of the detokenize call, pass `tokenGroupRedactions` via `Skyflow.RevealOptions`: +```swift +let revealOptions = Skyflow.RevealOptions( + tokenGroupRedactions: [ + Skyflow.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) +container.reveal(callback: revealCallback, options: revealOptions) +``` +This is a request-level setting - the redaction applies to every token in the named group, not to individual reveal elements. ### UI Error for Reveal Elements @@ -1992,9 +2002,7 @@ let cardNumberInput = Skyflow.RevealElementInput( labelStyles: labelStyles, errorTextStyles: errorTextStyles, label: "cardnumber", - altText: "XXXX XXXX XXXX XXXX", - redaction: "MASKED", - tokenGroupName: "deterministic_string" + altText: "XXXX XXXX XXXX XXXX" ) let cardNumberElement = container?.create(input: cardNumberInput) @@ -2036,8 +2044,15 @@ let revealCallback = Skyflow.RevealCallback( } ) +// Optional: apply a redaction to a token group as part of the detokenize call +let revealOptions = Skyflow.RevealOptions( + tokenGroupRedactions: [ + Skyflow.TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED") + ] +) + // Call reveal method on RevealContainer -container?.reveal(callback: revealCallback) +container?.reveal(callback: revealCallback, options: revealOptions) ``` diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index 07c53ec6..f044cf36 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -22,6 +22,7 @@ class CollectAndRevealViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func viewDidLoad() { @@ -157,7 +158,8 @@ class CollectAndRevealViewController: UIViewController { onSuccess: { response in print("reveal success:", response) }, onFailure: { error in print("reveal failure:", error) } ) - self.revealContainer?.reveal(callback: revealCallback) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) @@ -204,12 +206,23 @@ class CollectAndRevealViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "card_number").token, inputStyles: revealStyles, - label: "Card Number", - redaction: "REDACTED", - tokenGroupName: self.firstToken(tokens, column: "card_number").tokenGroupName + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -218,25 +231,19 @@ class CollectAndRevealViewController: UIViewController { let revealCVVtInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "CVV", - redaction: "MASKED", - tokenGroupName: self.firstToken(tokens, column: "cvv").tokenGroupName + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "cardholder_name").token, inputStyles: revealStyles, - label: "Card Holder Name", - redaction: "DEFAULT", - tokenGroupName: self.firstToken(tokens, column: "cardholder_name").tokenGroupName + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "expiry_month").token, inputStyles: revealStyles, - label: "Expiration Month", - redaction: "PLAIN_TEXT", - tokenGroupName: self.firstToken(tokens, column: "expiry_month").tokenGroupName + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index 27fbf016..1f5d622a 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -22,6 +22,7 @@ class ViewController: UIViewController { private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -174,7 +175,8 @@ class ViewController: UIViewController { onSuccess: { response in print("reveal success:", response) }, onFailure: { error in print("reveal failure:", error) } ) - self.revealContainer?.reveal(callback: revealCallback) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { let collectCallback = Skyflow.CollectCallback(onSuccess: updateSuccess, onFailure: updateFailure) @@ -235,12 +237,23 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = self.firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("REDACTED", forColumn: "card_number") + addTokenGroupRedaction("MASKED", forColumn: "cvv") + addTokenGroupRedaction("DEFAULT", forColumn: "cardholder_name") + addTokenGroupRedaction("PLAIN_TEXT", forColumn: "expiry_month") + let revealCardNumberInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "card_number").token ?? "", inputStyles: revealStyles, - label: "Card Number", - redaction: "REDACTED", - tokenGroupName: self.firstToken(tokens, column: "card_number").tokenGroupName + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -249,25 +262,19 @@ class ViewController: UIViewController { let revealCVVtInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "cvv").token ?? "", inputStyles: revealStyles, - label: "CVV", - redaction: "MASKED", - tokenGroupName: self.firstToken(tokens, column: "cvv").tokenGroupName + label: "CVV" ) self.revealCVV = self.revealContainer?.create(input: revealCVVtInput) let revealNameInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "cardholder_name").token ?? "", inputStyles: revealStyles, - label: "Card Holder Name", - redaction: "DEFAULT", - tokenGroupName: self.firstToken(tokens, column: "cardholder_name").tokenGroupName + label: "Card Holder Name" ) self.revealName = self.revealContainer?.create(input: revealNameInput) let revealExpirationMonthInput = Skyflow.RevealElementInput( token: self.firstToken(tokens, column: "expiry_month").token ?? "", inputStyles: revealStyles, - label: "Expiration Month", - redaction: "PLAIN_TEXT", - tokenGroupName: self.firstToken(tokens, column: "expiry_month").tokenGroupName + label: "Expiration Month" ) self.revealExpirationMonth = self.revealContainer?.create(input: revealExpirationMonthInput) let revealExpirationYearInput = Skyflow.RevealElementInput( diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/UpsertFeature/UpsertFeature/ViewController.swift index c8e61015..68a7e0ad 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -15,6 +15,7 @@ class ViewController: UIViewController { private var revealCvv: Label? private var revealButton: UIButton! private var revealed = false + private var tokenGroupRedactions: [Skyflow.TokenGroupRedaction] = [] override func loadView() { let view = UIView() @@ -154,7 +155,8 @@ class ViewController: UIViewController { onSuccess: { response in print("reveal success:", response) }, onFailure: { error in print("reveal failure:", error) } ) - self.revealContainer?.reveal(callback: revealCallback) + let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) + self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) } @objc func submitForm() { @@ -202,12 +204,21 @@ class ViewController: UIViewController { } else { self.revealed = true } + // tokenGroupRedactions: request-level redaction per token group, applied when + // reveal() is called (see revealForm()) - not per reveal element. + self.tokenGroupRedactions = [] + func addTokenGroupRedaction(_ redaction: String, forColumn column: String) { + if let tokenGroupName = firstToken(tokens, column: column).tokenGroupName { + self.tokenGroupRedactions.append(Skyflow.TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) + } + } + addTokenGroupRedaction("DEFAULT", forColumn: "cardnumber") + addTokenGroupRedaction("DEFAULT", forColumn: "cvv") + let revealCardNumberInput = Skyflow.RevealElementInput( token: firstToken(tokens, column: "cardnumber").token, inputStyles: revealStyles, - label: "Card Number", - redaction: "DEFAULT", - tokenGroupName: firstToken(tokens, column: "cardnumber").tokenGroupName + label: "Card Number" ) self.revealCardNumber = self.revealContainer?.create( input: revealCardNumberInput, @@ -216,9 +227,7 @@ class ViewController: UIViewController { let revealCvvInput = Skyflow.RevealElementInput( token: firstToken(tokens, column: "cvv").token, inputStyles: revealStyles, - label: "Cvv", - redaction: "DEFAULT", - tokenGroupName: firstToken(tokens, column: "cvv").tokenGroupName + label: "Cvv" ) self.revealCvv = self.revealContainer?.create( input: revealCvvInput, diff --git a/Sources/Skyflow/core/Client.swift b/Sources/Skyflow/core/Client.swift index 7505f499..d956f3cc 100644 --- a/Sources/Skyflow/core/Client.swift +++ b/Sources/Skyflow/core/Client.swift @@ -164,7 +164,7 @@ public class Client { onFailureHandler: { } ) - self.apiClient.get(records: list, callback: logCallback, contextOptions: tempContextOptions) + self.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } else { callRevealOnFailure(callback: callback, errorObject: ErrorCodes.INVALID_RECORDS_TYPE().getErrorObject(contextOptions: tempContextOptions)) } diff --git a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift index 755d47de..0a689f8d 100644 --- a/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift +++ b/Sources/Skyflow/reveal/FlowVaultDetokenizeRequestBody.swift @@ -11,15 +11,9 @@ internal class FlowVaultDetokenizeRequestBody { let tokens = records.map { $0.token } var body: [String: Any] = ["vaultID": vaultID, "tokens": tokens] if let tokenGroupRedactions = tokenGroupRedactions, !tokenGroupRedactions.isEmpty { - // Dedupe by tokenGroupName, keeping the last occurrence so explicitly-passed - // options can override anything derived from individual reveal elements. - var redactionByGroup: [String: String] = [:] - var order: [String] = [] - for tgr in tokenGroupRedactions { - if redactionByGroup[tgr.tokenGroupName] == nil { order.append(tgr.tokenGroupName) } - redactionByGroup[tgr.tokenGroupName] = tgr.redaction - } - body["tokenGroupRedactions"] = order.map { ["tokenGroupName": $0, "redaction": redactionByGroup[$0]!] } + // Pass through exactly what was collected from the mounted elements, duplicates + // and all - the API validates tokenGroupRedactions, not the SDK. + body["tokenGroupRedactions"] = tokenGroupRedactions.map { ["tokenGroupName": $0.tokenGroupName, "redaction": $0.redaction] } } return body } diff --git a/Sources/Skyflow/reveal/RevealContainer.swift b/Sources/Skyflow/reveal/RevealContainer.swift index 71810ab6..c3a48380 100644 --- a/Sources/Skyflow/reveal/RevealContainer.swift +++ b/Sources/Skyflow/reveal/RevealContainer.swift @@ -65,12 +65,6 @@ public extension Container { list.append(RevealRequestRecord(token: id)) } } - var tokenGroupRedactions: [TokenGroupRedaction] = [] - for element in self.revealElements { - if let tokenGroupName = element.revealInput.tokenGroupName, let redaction = element.revealInput.redaction { - tokenGroupRedactions.append(TokenGroupRedaction(tokenGroupName: tokenGroupName, redaction: redaction)) - } - } let logCallback = LogCallback(clientCallback: revealValueCallback, contextOptions: tempContextOptions, onSuccessHandler: { Log.info(message: .REVEAL_SUBMIT_SUCCESS, contextOptions: tempContextOptions) @@ -78,7 +72,7 @@ public extension Container { onFailureHandler: { } ) - self.skyflow.apiClient.get(records: list, tokenGroupRedactions: tokenGroupRedactions.isEmpty ? nil : tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) + self.skyflow.apiClient.get(records: list, tokenGroupRedactions: options?.tokenGroupRedactions, callback: logCallback, contextOptions: tempContextOptions) } } } diff --git a/Sources/Skyflow/reveal/RevealElementInput.swift b/Sources/Skyflow/reveal/RevealElementInput.swift index 7138b857..3bf4d7f3 100644 --- a/Sources/Skyflow/reveal/RevealElementInput.swift +++ b/Sources/Skyflow/reveal/RevealElementInput.swift @@ -13,17 +13,13 @@ public struct RevealElementInput { internal var errorTextStyles: Styles? internal var label: String internal var altText: String - internal var redaction: String? - internal var tokenGroupName: String? - public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "", redaction: String? = nil, tokenGroupName: String? = nil) { + public init(token: String = "", inputStyles: Styles = Styles(), labelStyles: Styles = Styles(), errorTextStyles: Styles = Styles(), label: String, altText: String = "") { self.token = token self.inputStyles = inputStyles self.labelStyles = labelStyles self.errorTextStyles = errorTextStyles self.label = label self.altText = altText - self.redaction = redaction - self.tokenGroupName = tokenGroupName } } diff --git a/Sources/Skyflow/reveal/RevealOptions.swift b/Sources/Skyflow/reveal/RevealOptions.swift index 18e1d33c..eaf1de74 100644 --- a/Sources/Skyflow/reveal/RevealOptions.swift +++ b/Sources/Skyflow/reveal/RevealOptions.swift @@ -12,5 +12,9 @@ import Foundation public struct RevealOptions { - public init() {} + public let tokenGroupRedactions: [TokenGroupRedaction]? + + public init(tokenGroupRedactions: [TokenGroupRedaction]? = nil) { + self.tokenGroupRedactions = tokenGroupRedactions + } } diff --git a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift index 88d78772..d981c078 100644 --- a/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift +++ b/Tests/skyflow-iOS-revealTests/skyflow_iOS_revealTests.swift @@ -59,20 +59,18 @@ class skyflow_iOS_revealTests: XCTestCase { XCTAssertEqual(revealElementInput.label, "RevealElement") } - func testRevealElementInputWithRedactionAndTokenGroupName() { - let revealElementInput = RevealElementInput(token: revealTestId, label: "RevealElement", redaction: "MASKED", tokenGroupName: "deterministic_string") + func testRevealOptionsCarriesTokenGroupRedactions() { + let options = RevealOptions(tokenGroupRedactions: [TokenGroupRedaction(tokenGroupName: "deterministic_string", redaction: "MASKED")]) - XCTAssertEqual(revealElementInput.redaction, "MASKED") - XCTAssertEqual(revealElementInput.tokenGroupName, "deterministic_string") + XCTAssertEqual(options.tokenGroupRedactions?.count, 1) + XCTAssertEqual(options.tokenGroupRedactions?.first?.tokenGroupName, "deterministic_string") + XCTAssertEqual(options.tokenGroupRedactions?.first?.redaction, "MASKED") } - func testRevealElementCarriesRedactionAndTokenGroupNameThroughContainer() { - let revealContainer = skyflow.container(type: ContainerType.REVEAL, options: nil) - let revealElementInput = RevealElementInput(token: revealTestId, label: "RevealElement", redaction: "PLAIN_TEXT", tokenGroupName: "nondeterministic_string") - _ = revealContainer?.create(input: revealElementInput, options: RevealElementOptions()) + func testRevealOptionsDefaultsToNilTokenGroupRedactions() { + let options = RevealOptions() - XCTAssertEqual(revealContainer?.revealElements[0].revealInput.redaction, "PLAIN_TEXT") - XCTAssertEqual(revealContainer?.revealElements[0].revealInput.tokenGroupName, "nondeterministic_string") + XCTAssertNil(options.tokenGroupRedactions) } func testCreateSkyflowRevealContainer() { diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift index a24d7d22..66730bb7 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_revealUtilTests.swift @@ -204,7 +204,7 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { } } - func testConstructV2DetokenizeRequestBodyDedupesTokenGroupRedactions() { + func testConstructV2DetokenizeRequestBodyPassesThroughDuplicateTokenGroupRedactions() { let records = [RevealRequestRecord(token: "token1")] let tokenGroupRedactions = [ TokenGroupRedaction(tokenGroupName: "group1", redaction: "MASKED"), @@ -213,9 +213,11 @@ final class skyflow_iOS_revealUtilTests: XCTestCase { let result = FlowVaultDetokenizeRequestBody.createRequestBody(vaultID: "vault123", records: records, tokenGroupRedactions: tokenGroupRedactions) let redactions = result["tokenGroupRedactions"] as! [[String: Any]] - XCTAssertEqual(redactions.count, 1) + XCTAssertEqual(redactions.count, 2) XCTAssertEqual(redactions[0]["tokenGroupName"] as? String, "group1") - XCTAssertEqual(redactions[0]["redaction"] as? String, "PLAIN_TEXT") + XCTAssertEqual(redactions[0]["redaction"] as? String, "MASKED") + XCTAssertEqual(redactions[1]["tokenGroupName"] as? String, "group1") + XCTAssertEqual(redactions[1]["redaction"] as? String, "PLAIN_TEXT") } func testGetTokensToErrors() { From 7d38bd6feb51867f38ffb628fda6e1b5ea91299c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 29 Jul 2026 17:30:23 +0530 Subject: [PATCH 12/14] SK-3005 update readme --- README.md | 151 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 126 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index c3155ced..082049bb 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ let collectElementInput = Skyflow.CollectElementInput( The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. **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() + The `inputStyles` parameter accepts a Skyflow.Styles object which consists of multiple `Skyflow.Styles` objects which should be applied to the form element in the following states: @@ -390,7 +390,7 @@ let nonPCIRecords = Skyflow.AdditionalFields(records: [ ]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) @@ -500,19 +500,41 @@ container?.collect(callback: insertCallback, options: collectOptions) ] } ``` -**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode` - see the [partial error response example](#sample-partial-error-response) below. +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +Successful and failed records are both returned together in the same `records` array, each carrying its own `httpCode`: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` #### If the entire request fails (for example, the vault itself can't be found): +This is the API's raw wire-format error - `onFailure` doesn't hand you this dictionary as-is, it's normalized into a `Skyflow.SkyflowError` object with these same fields as flat properties (no `error` wrapper key): ```json { - "error": { "grpcCode": 5, "httpCode": 404, "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", "httpStatus": "Not Found", "details": [] } -} ``` `Skyflow.CollectCallback`/`Skyflow.RevealCallback`'s `onFailure` is typed `(Skyflow.SkyflowError) -> Void` - this shape (and every other failure, including client-side validation) is always delivered as a `Skyflow.SkyflowError`, with `httpCode`, `message`, `grpcCode`, `httpStatus`, and `details` all available as direct properties (`grpcCode`/`httpStatus`/`details` are only populated for this whole-request-failure shape - `nil` for validation failures): ```swift @@ -555,7 +577,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 which is described below. @@ -602,7 +623,7 @@ let nonPCIRecords = Skyflow.AdditionalFields(records: [ ]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) @@ -713,6 +734,42 @@ container?.collect(callback: insertCallback, options: collectOptions) ] } ``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "431eaa6c-5c15-4513-aa15-29f50babe882", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` + +#### If the entire request fails +This is the API's error - `onFailure` doesn't hand you this dictionary as-is, it's normalized into a `Skyflow.SkyflowError` object with these same fields as flat properties (no `error` wrapper key): +```json +{ + "grpcCode": 5, + "httpCode": 404, + "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", + "httpStatus": "Not Found", + "details": [] +} +``` +`Skyflow.CollectCallback`'s `onFailure` is typed `(Skyflow.SkyflowError) -> Void` - see [If the entire request fails](#if-the-entire-request-fails-for-example-the-vault-itself-cant-be-found) above for how to read the properties on `Skyflow.SkyflowError`. ## Validations @@ -1175,7 +1232,7 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { } ``` ### Step 4: Collect data from elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: Skyflow.Callback, options: Skyflow.CollectOptions? = Skyflow.CollectOptions())` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' @@ -1189,7 +1246,7 @@ let nonPCIRecords = Skyflow.AdditionalFields(records: [ ]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) @@ -1325,6 +1382,29 @@ container?.collect(callback: insertCallback, options: collectOptions) ] } ``` +**Note:** Successful and failed records are both returned in the same `records` array, each carrying its own `httpCode`. + +#### Sample partial error response: +```json +{ + "records": [ + { + "tableName": "cards", + "skyflowID": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "fields": { + "cardNumber": [{"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"}] + }, + "httpCode": 200 + }, + { + "error": "Invalid request. Table name table not present for record. Specify a valid table name.", + "skyflowID": null, + "tableName": "", + "httpCode": 400 + } + ] +} +``` [For information on validations, see validations.](#validations) @@ -1521,7 +1601,7 @@ let lengthRule = LengthMatchRule( // Update validations and placeholder property on cardHolderName. cardHolderName.update(update: CollectElementInput( placeholder: "cardHolderName", - validations: ValidationSet(rules: [lengthRule])) + validations: ValidationSet(rules: [lengthRule]) )) ``` @@ -1559,7 +1639,7 @@ do { // Subscribing to the `SUBMIT` event, which gets triggered when the user hits `enter` key in any container element input. composableContainer?.on(eventName: .SUBMIT){ // Your implementation when the SUBMIT(enter) event occurs. - print('Submit Event Listener is being Triggered.') + print("Submit Event Listener is being Triggered.") } ``` @@ -1643,7 +1723,7 @@ func clearFieldsOnSubmit(_ elements: [TextField]) { } ``` ### Step 4: Update data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: Skyflow.Callback, options: Skyflow.CollectOptions? = Skyflow.CollectOptions())` method on the container object. The options parameter takes a `Skyflow.CollectOptions` object as shown below: - `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' @@ -1657,7 +1737,7 @@ let nonPCIRecords = Skyflow.AdditionalFields(records: [ ]) // Upsert let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardNumber"], updateType: .UPDATE)] -// Send the non-PCI records as additionalFields of InsertOptions (optional) and apply upsert using `upsert` field of InsertOptions (optional) +// Send the non-PCI records as additionalFields of CollectOptions (optional) and apply upsert using `upsert` field of CollectOptions (optional) let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) @@ -1821,17 +1901,16 @@ Successful and failed records are both returned together in the same `records` a } ``` -If the entire request fails (for example, the vault itself can't be found), `onFailure` receives a single structured error instead of a `records` array: +If the entire request fails (for example, the vault itself can't be found), `onFailure` receives a single structured `Skyflow.SkyflowError` instead of a `records` array. +Below is the API's error that `SkyflowError` is built from - the object itself exposes these as flat properties (`grpcCode`, `httpCode`, `message`, `httpStatus`, `details`): ```json -{ - "error": { + "grpcCode": 5, "httpCode": 404, "message": "Invalid request. Vault not found for vaultID: sd0ff0b064c04faabd4d392512b2e5. Specify a valid vaultID. - request-id: cb397-8521-42c2-870c-92dbeec", "httpStatus": "Not Found", "details": [] } -} ``` # Securely revealing data client-side @@ -1862,7 +1941,6 @@ let revealElementInput = Skyflow.RevealElementInput( ) ``` `Note`: -- `token` is optional only if it is being used in invokeConnection() - To apply a redaction to a token group as part of the detokenize call, use `tokenGroupRedactions` on `Skyflow.RevealOptions` (passed to `reveal(callback:options:)`) rather than on the individual `RevealElementInput` - see [Step 4: Reveal data](#step-4-reveal-data). If not provided, the vault-configured default redaction is applied. Supported redaction values are `PLAIN_TEXT`, `MASKED`, `REDACTED` and `DEFAULT`. @@ -2056,21 +2134,45 @@ container?.reveal(callback: revealCallback, options: revealOptions) ``` -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. - #### Sample Response: ```json { "records": [ { "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - "value": "4111111111111111", - "tokenGroupName": "deterministic_string" + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 }, { "token": "89024714-6a26-4256-b9d4-55ad69aa4047", - "value": "123", - "tokenGroupName": "deterministic_string" + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + } + ] +} +``` + +#### Sample Partial Error Response: +Some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed - both are returned together in the same `records` array, each carrying its own `httpCode`. A successful record carries `metadata` (e.g. `skyflowId`, `tableName`), a failed one carries `error` instead: +```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", @@ -2080,7 +2182,6 @@ The response below shows that some tokens assigned to the reveal elements get re ] } ``` -**Note:** Successful and failed tokens are both returned in the same `records` array, each entry distinguished by the presence of an `error` key. ## Reporting a Vulnerability From 443b4931f2f276d29d7d8b2d1586e9926ea7d420 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 29 Jul 2026 18:03:26 +0530 Subject: [PATCH 13/14] SK-3005 added tests --- .../Skyflow_iOS_collectErrorTests.swift | 34 +++++++++++++++++++ .../skyflow_iOS_utilTests.swift | 12 +++++++ 2 files changed, 46 insertions(+) diff --git a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift index 8b480dcb..813668ce 100644 --- a/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift +++ b/Tests/skyflow-iOS-errorTests/Skyflow_iOS_collectErrorTests.swift @@ -616,6 +616,40 @@ final class Skyflow_iOS_collectErrorTests: XCTestCase { XCTAssertEqual(update.count, 2) } + // Not currently reachable from any documented flow (README only shows ElementValueMatchRule + // used to allow a duplicate plain/insert element, not two update-by-skyflowId elements), but + // the code path exists in FlowVaultCollectRequestBody's update branch too - covering it now + // in case a future update UI (e.g. confirm-password-style re-entry on an editable field) relies on it. + func testCreateRequestBodyElementValueMatchRuleBypassesUpdateDuplicate() { + let window = UIWindow() + let container = skyflow.container(type: ContainerType.COLLECT, options: nil) + let options = CollectElementOptions(required: false) + + let updateInput1 = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, skyflowId: "id1") + let updateElement1 = container?.create(input: updateInput1, options: options) + updateElement1?.textField.secureText = "John" + updateElement1?.textFieldDidEndEditing(updateElement1!.textField) + window.addSubview(updateElement1!) + + var vs = ValidationSet() + vs.add(rule: ElementValueMatchRule(element: updateElement1!, error: "ELEMENT NOT MATCHING")) + let updateInput2 = CollectElementInput(table: "persons", column: "name", placeholder: "name", type: .CARDHOLDER_NAME, validations: vs, skyflowId: "id1") + let updateElement2 = container?.create(input: updateInput2, options: options) + updateElement2?.textField.secureText = "Jane" + updateElement2?.textFieldDidEndEditing(updateElement2!.textField) + window.addSubview(updateElement2!) + + let callback = DemoAPICallback(expectation: XCTestExpectation(description: "Should not fail")) + let requestBody = FlowVaultCollectRequestBody.createRequestBody(elements: [updateElement1!, updateElement2!], callback: callback, contextOptions: ContextOptions()) + + XCTAssertNotNil(requestBody) + let update = requestBody?["update"] as! [String: Any] + XCTAssertEqual(update.count, 1) + let entry = update["id1"] as! [String: Any] + // Second element's value is skipped (continue), not merged over the first's. + XCTAssertEqual((entry["fields"] as! [String: String])["name"], "John") + } + func testInsertEmptyVaultURL() { let expectation = XCTestExpectation(description: "Insert with empty vaultURL should fail") let callback = DemoAPICallback(expectation: expectation) diff --git a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift index ce25a027..28acb539 100644 --- a/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift +++ b/Tests/skyflow-iOS-utilTests/skyflow_iOS_utilTests.swift @@ -78,6 +78,18 @@ final class skyflow_iOS_utilTests: XCTestCase { XCTAssertEqual(upsertPayload["updateType"] as! String, "REPLACE") } + // Not exercised by any current call site (UpsertOption always supplies a concrete + // updateType today), but the nil default is part of the public API - confirms + // "updateType" is omitted from the wire payload rather than serialized as null. + func testConstructV2RequestBodyWithUpsertAndNilUpdateType() { + let upsert = [UpsertOption(table: "table", uniqueColumns: ["field1"])] + let result = FlowVaultInsertRequestBody.createRequestBody(vaultID: "vault123", records: ["records": [["table": "table", "fields": ["field1": "value1"]]]], options: FlowVaultICOptions(upsert: upsert)) + let records = result["records"] as! [[String: Any]] + let upsertPayload = records[0]["upsert"] as! [String: Any] + XCTAssertEqual(upsertPayload["uniqueColumns"] as! [String], ["field1"]) + XCTAssertNil(upsertPayload["updateType"]) + } + // Dormant v1 PDB helpers on APIClient (untyped upsert), kept for potential future PDB reuse. func testGetUniqueColumn() { From 17f8c55e438fdb48f3583677d43ab3b49ea4145c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Wed, 29 Jul 2026 19:41:44 +0530 Subject: [PATCH 14/14] SK-3005 update callbacks in readme --- README.md | 140 ++++++++++++++---- .../xcshareddata/swiftpm/Package.resolved | 15 ++ .../CollectAndRevealViewController.swift | 18 ++- .../ComposableElements/ViewController.swift | 18 ++- .../UpsertFeature/ViewController.swift | 16 +- 5 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/README.md b/README.md index 082049bb..2ad2537e 100644 --- a/README.md +++ b/README.md @@ -395,8 +395,18 @@ let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardN let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in print(response) }, - onFailure: { error in print(error) } + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } ) container?.collect(callback: insertCallback, options: options) @@ -465,11 +475,17 @@ let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, ups // Skyflow.SkyflowError whenever the vault returns that structured shape (see below); // otherwise onFailure receives the raw error (e.g. a validation SkyflowError) unchanged. let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in - print(response) + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } }, - onFailure: { error in - print(error) + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ) @@ -628,8 +644,18 @@ let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardN let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords) let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in print(response) }, - onFailure: { error in print(error) } + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } ) container?.collect(callback: insertCallback, options: options) ``` @@ -697,11 +723,17 @@ let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, ups // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). // See "If the entire request fails" above for how onFailure surfaces a typed Skyflow.SkyflowError. let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in - print(response) + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } }, - onFailure: { error in - print(error) + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ) @@ -1251,8 +1283,18 @@ let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardN let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in print(response) }, - onFailure: { error in print(error) } + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } ) container?.collect(callback: insertCallback, options: options) ``` @@ -1347,11 +1389,17 @@ let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, ups // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in - print(response) + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } }, - onFailure: { error in - print(error) + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ) @@ -1742,8 +1790,18 @@ let upsertOptions = [Skyflow.UpsertOption(table: "cards", uniqueColumns: ["cardN let options = Skyflow.CollectOptions(additionalFields: nonPCIRecords, upsert: upsertOptions) let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in print(response) }, - onFailure: { error in print(error) } + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } ) container?.collect(callback: insertCallback, options: options) ``` @@ -1840,11 +1898,17 @@ let collectOptions = Skyflow.CollectOptions(additionalFields: nonPCIRecords, ups // Initialize a Skyflow.CollectCallback - required by CollectContainer's collect(callback:options:). let insertCallback = Skyflow.CollectCallback( - onSuccess: { response in - print(response) + onSuccess: { (response: Skyflow.CollectResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("success:", record) + } + } }, - onFailure: { error in - print(error) + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ) @@ -2019,8 +2083,18 @@ Elements used for revealing data are mounted to the screen the same way as Eleme When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: ```swift let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in print(response) }, - onFailure: { error in print(error) } + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } + }, + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") + } ) container.reveal(callback: revealCallback) ``` @@ -2114,11 +2188,17 @@ cvvElement!.resetError() // Initialize a Skyflow.RevealCallback - required by RevealContainer's reveal(callback:options:). let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in - print(response) + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record) + } + } }, - onFailure: { error in - print(error) + onFailure: { (skyflowError: Skyflow.SkyflowError) in + print(skyflowError.httpCode, skyflowError.message, skyflowError.grpcCode ?? "", skyflowError.httpStatus ?? "", skyflowError.details ?? "") } ) diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..31459b56 --- /dev/null +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "9d7a641543031e54b3470012de29bf3cd9a042920b5fe71a347a8d0e07ccefe2", + "pins" : [ + { + "identity" : "skyflow-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/skyflowapi/skyflow-iOS.git", + "state" : { + "revision" : "cb30c6b6b8a36105fa4bf8ba6a879af8b777d2c7", + "version" : "1.25.1" + } + } + ], + "version" : 3 +} diff --git a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift index f044cf36..d360b5e4 100644 --- a/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift +++ b/Samples/CollectAndRevealSample/CollectAndRevealSample/CollectAndRevealViewController.swift @@ -155,8 +155,16 @@ class CollectAndRevealViewController: UIViewController { } @objc func revealForm() { let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in print("reveal success:", response) }, - onFailure: { error in print("reveal failure:", error) } + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } ) let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) @@ -178,12 +186,12 @@ class CollectAndRevealViewController: UIViewController { } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") } // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { diff --git a/Samples/ComposableElements/ComposableElements/ViewController.swift b/Samples/ComposableElements/ComposableElements/ViewController.swift index 1f5d622a..9bc833bf 100644 --- a/Samples/ComposableElements/ComposableElements/ViewController.swift +++ b/Samples/ComposableElements/ComposableElements/ViewController.swift @@ -172,8 +172,16 @@ class ViewController: UIViewController { } @objc func revealForm() { let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in print("reveal success:", response) }, - onFailure: { error in print("reveal failure:", error) } + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } ) let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) @@ -209,12 +217,12 @@ class ViewController: UIViewController { } print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - if((error as AnyObject).contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future + internal func updateFailure(error: Skyflow.SkyflowError) { + if(error.message.contains("Invalid Bearer token") && retryCount <= 2){ // To do, it will be replaced with error code in the future retryCount += 1 submitForm() } - print("Failed Operation", error) + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") } // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts. internal func firstToken(_ fields: [String: Any], column: String) -> (token: String, tokenGroupName: String?) { diff --git a/Samples/UpsertFeature/UpsertFeature/ViewController.swift b/Samples/UpsertFeature/UpsertFeature/ViewController.swift index 68a7e0ad..1e82b7cf 100644 --- a/Samples/UpsertFeature/UpsertFeature/ViewController.swift +++ b/Samples/UpsertFeature/UpsertFeature/ViewController.swift @@ -152,8 +152,16 @@ class ViewController: UIViewController { @objc func revealForm() { let revealCallback = Skyflow.RevealCallback( - onSuccess: { response in print("reveal success:", response) }, - onFailure: { error in print("reveal failure:", error) } + onSuccess: { (response: Skyflow.RevealResponse) in + for record in response.records { + if let error = record.error { + print("reveal failed:", error, "httpCode:", record.httpCode) + } else { + print("revealed:", record.token ?? "", record.tokenGroupName ?? "", record.metadata ?? [:]) + } + } + }, + onFailure: { (error: Skyflow.SkyflowError) in print("reveal failure:", error.httpCode, error.message) } ) let revealOptions = Skyflow.RevealOptions(tokenGroupRedactions: self.tokenGroupRedactions.isEmpty ? nil : self.tokenGroupRedactions) self.revealContainer?.reveal(callback: revealCallback, options: revealOptions) @@ -178,8 +186,8 @@ class ViewController: UIViewController { print("Successfully got response:", response) } - internal func updateFailure(error: Any) { - print("Failed Operation", error) + internal func updateFailure(error: Skyflow.SkyflowError) { + print("Failed Operation", error.httpCode, error.message, error.grpcCode ?? "", error.httpStatus ?? "", error.details ?? "") } // fields is [String: Any] - column name -> array of {"token","tokenGroupName"} dicts.