From c5be3260424424deb2424c8d811bf60963ef26bf Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 8 Sep 2026 12:43:38 +0530 Subject: [PATCH 1/2] SK-3131 Added unary operations --- .../java/com/skyflow/errors/ErrorMessage.java | 9 + .../java/com/skyflow/enums/InterfaceName.java | 2 + .../main/java/com/skyflow/utils/Utils.java | 314 +++++++++++++++++- .../utils/validations/Validations.java | 295 ++++++++++++++++ .../vault/controller/VaultController.java | 261 ++++++++++++++- .../skyflow/vault/data/ColumnRedactions.java | 40 +++ .../com/skyflow/vault/data/DeleteOptions.java | 33 ++ .../com/skyflow/vault/data/DeleteRequest.java | 59 ++++ .../skyflow/vault/data/DeleteResponse.java | 24 ++ .../vault/data/DeleteResponseRecord.java | 37 +++ .../com/skyflow/vault/data/GetOptions.java | 33 ++ .../com/skyflow/vault/data/GetRequest.java | 101 ++++++ .../skyflow/vault/data/GetRequestRecord.java | 79 +++++ .../com/skyflow/vault/data/GetResponse.java | 24 ++ .../skyflow/vault/data/GetResponseRecord.java | 15 + .../com/skyflow/vault/data/QueryOptions.java | 33 ++ .../com/skyflow/vault/data/QueryRequest.java | 28 ++ .../com/skyflow/vault/data/QueryResponse.java | 31 ++ .../vault/data/QueryResponseRecord.java | 26 ++ .../com/skyflow/vault/data/UpdateOptions.java | 33 ++ .../com/skyflow/vault/data/UpdateRequest.java | 56 ++++ .../vault/data/UpdateRequestRecord.java | 66 ++++ .../skyflow/vault/data/UpdateResponse.java | 24 ++ .../vault/data/UpdateResponseRecord.java | 15 + 24 files changed, 1611 insertions(+), 27 deletions(-) create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/ColumnRedactions.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/DeleteOptions.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/GetRequestRecord.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/QueryResponseRecord.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/UpdateOptions.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/UpdateRequestRecord.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java create mode 100644 flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index f7f65aa3..b344dd69 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -98,6 +98,9 @@ public enum ErrorMessage { EmptyTokenInDetokenizeData("%s0 Validation error. Invalid data tokens. Specify a valid data token."), TokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), + // Delete + DeleteRequestNull("%s0 Validation error. DeleteRequest object is null. Specify a valid DeleteRequest object."), + // Delete Tokens DeleteTokensRequestNull("%s0 Validation error. DeleteTokensRequest object is null. Specify a valid DeleteTokensRequest object."), EmptyDeleteTokensData("%s0 Validation error. Tokens list is empty. Specify at least one token to delete."), @@ -105,6 +108,7 @@ public enum ErrorMessage { DeleteTokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), // Get + GetRequestNull("%s0 Validation error. GetRequest object is null. Specify a valid GetRequest object."), IdsKeyError("%s0 Validation error. 'ids' key is missing from the payload. Specify an 'ids' key."), EmptyIds("%s0 Validation error. 'ids' can't be empty. Specify at least one id."), EmptyIdInIds("%s0 Validation error. Invalid id in 'ids'. Specify a valid id."), @@ -141,8 +145,13 @@ public enum ErrorMessage { SkyflowIdKeyError("%s0 Validation error. 'skyflow_id' is missing from the data payload. Specify a 'skyflow_id'."), InvalidSkyflowIdType("%s0 Validation error. Invalid type for 'skyflow_id' in data payload. Specify 'skyflow_id' as a string."), EmptySkyflowId("%s0 Validation error. 'skyflow_id' can't be empty. Specify a skyflow id."), + UpdateRequestNull("%s0 Validation error. UpdateRequest object is null. Specify a valid UpdateRequest object."), + UpdateRecordNull("%s0 Validation error. UpdateRequestRecord object in the list is null. Specify a valid UpdateRequestRecord object."), + RecordSkyflowIdKeyError("%s0 Validation error. 'skyflowId' key is missing from the record. Specify a 'skyflowId' key."), + EmptySkyflowIdInRecord("%s0 Validation error. 'skyflowId' can't be empty in the record. Specify a valid skyflow ID."), // Query + QueryRequestNull("%s0 Validation error. QueryRequest object is null. Specify a valid QueryRequest object."), QueryKeyError("%s0 Validation error. 'query' key is missing from the payload. Specify a 'query' key."), EmptyQuery("%s0 Validation error. 'query' can't be empty. Specify a query"), diff --git a/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java b/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java index 475f8ad2..7681bc87 100644 --- a/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java +++ b/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java @@ -2,8 +2,10 @@ public enum InterfaceName { INSERT("insert"), + UPDATE("update"), DETOKENIZE("detokenize"), DELETE("delete tokens"), + DELETE_RECORDS("delete"), TOKENIZE("tokenize"), QUERY("query"), GET("get"); diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index cfd8195f..51ca252a 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -2,7 +2,12 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import com.google.gson.JsonObject; import com.skyflow.config.VaultConfig; @@ -14,6 +19,12 @@ import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1UpdateRequest; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import com.skyflow.generated.rest.types.FlowEnumUpdateType; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; @@ -25,43 +36,62 @@ import com.skyflow.generated.rest.types.V1InsertRecordData; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1UpdateRecordData; +import com.skyflow.generated.rest.types.V1ColumnRedactions; +import com.skyflow.generated.rest.types.V1GetRequestData; +import com.skyflow.generated.rest.types.V1GetResponse; +import com.skyflow.generated.rest.types.V1UniqueValue; +import com.skyflow.generated.rest.types.V1DeleteResponse; +import com.skyflow.generated.rest.types.V1DeleteResponseObject; +import com.skyflow.generated.rest.types.V1UpdateResponse; import com.skyflow.generated.rest.types.V1TokenGroupRedactions; import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.ErrorLogs; import com.skyflow.utils.logger.LogUtil; import com.skyflow.vault.data.BulkDeleteTokensRequest; -import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRequestRecord; -import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.DeleteTokensRecord; -import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; import com.skyflow.vault.data.BulkDetokenizeResponseRecord; -import com.skyflow.vault.data.DetokenizeMetadata; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.DetokenizeMetadata; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.DetokenizeResponseRecord; import com.skyflow.vault.data.ErrorRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.InsertResponseRecord; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; +import com.skyflow.vault.data.UpdateResponse; +import com.skyflow.vault.data.UpdateResponseRecord; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.DeleteResponseRecord; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.GetResponseRecord; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.QueryResponseRecord; import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.UpsertOptions; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - public final class Utils extends BaseUtils { // Spellings the vault has used for the per-record status, in precedence order. @@ -179,6 +209,137 @@ private static String extractRequestId(Map> headers) { return (ids == null || ids.isEmpty()) ? null : ids.get(0); } + public static V1GetRequest getGetRequestBody(GetRequest request, VaultConfig config) { + V1GetRequest.Builder builder = V1GetRequest.builder().vaultId(config.getVaultId()); + + // Multi-table mode: Validations has already rejected the case where both this and the + // single-table fields below are set, so their presence/absence is mutually exclusive. + if (request.getRecords() != null && !request.getRecords().isEmpty()) { + List recordDataList = new ArrayList<>(); + for (GetRequestRecord record : request.getRecords()) { + recordDataList.add(toV1GetRequestData(record)); + } + return builder.records(recordDataList).build(); + } + + if (hasText(request.getTable())) { + builder.tableName(request.getTable()); + } + if (request.getIds() != null && !request.getIds().isEmpty()) { + builder.skyflowIDs(request.getIds()); + } + if (request.getFields() != null && !request.getFields().isEmpty()) { + builder.columns(request.getFields()); + } + if (request.getColumnRedactions() != null && !request.getColumnRedactions().isEmpty()) { + builder.columnRedactions(toV1ColumnRedactionsList(request.getColumnRedactions())); + } + if (request.getUniqueValues() != null && !request.getUniqueValues().isEmpty()) { + builder.uniqueValues(toV1UniqueValueList(request.getUniqueValues())); + } + if (request.getLimit() != null) { + builder.limit(request.getLimit()); + } + if (request.getOffset() != null) { + builder.offset(request.getOffset()); + } + return builder.build(); + } + + private static V1GetRequestData toV1GetRequestData(GetRequestRecord record) { + V1GetRequestData.Builder data = V1GetRequestData.builder().tableName(record.getTable()); + if (record.getIds() != null && !record.getIds().isEmpty()) { + data.skyflowIDs(record.getIds()); + } + if (record.getFields() != null && !record.getFields().isEmpty()) { + data.columns(record.getFields()); + } + if (record.getColumnRedactions() != null && !record.getColumnRedactions().isEmpty()) { + data.columnRedactions(toV1ColumnRedactionsList(record.getColumnRedactions())); + } + if (record.getUniqueValues() != null && !record.getUniqueValues().isEmpty()) { + data.uniqueValues(toV1UniqueValueList(record.getUniqueValues())); + } + return data.build(); + } + + private static List toV1ColumnRedactionsList(List columnRedactions) { + List list = new ArrayList<>(); + for (ColumnRedactions columnRedaction : columnRedactions) { + list.add(V1ColumnRedactions.builder() + .columnName(columnRedaction.getColumnName()) + .redaction(columnRedaction.getRedaction()) + .build()); + } + return list; + } + + private static List toV1UniqueValueList(List> uniqueValues) { + List list = new ArrayList<>(); + for (Map uniqueValue : uniqueValues) { + list.add(V1UniqueValue.builder().data(uniqueValue).build()); + } + return list; + } + + public static V1DeleteRequest getDeleteRequestBody(DeleteRequest request, VaultConfig config) { + V1DeleteRequest.Builder builder = V1DeleteRequest.builder() + .vaultId(config.getVaultId()) + .tableName(request.getTable()); + if (request.getIds() != null && !request.getIds().isEmpty()) { + builder.skyflowIDs(request.getIds()); + } + if (request.getUniqueValues() != null && !request.getUniqueValues().isEmpty()) { + builder.uniqueValues(toV1UniqueValueList(request.getUniqueValues())); + } + return builder.build(); + } + + public static V1UpdateRequest getUpdateRequestBody(UpdateRequest request, VaultConfig config) { + List updateRecordDataList = new ArrayList<>(); + for (UpdateRequestRecord record : request.getRecords()) { + V1UpdateRecordData.Builder data = V1UpdateRecordData.builder() + .skyflowId(record.getSkyflowId()) + .data(record.getData()); + if (record.getTokens() != null && !record.getTokens().isEmpty()) { + data.tokens(record.getTokens()); + } + // A blank record-level table name counts as absent, matching validateInsertRequest's + // handling of the same table/record split. + if (hasText(record.getTableName())) { + data.tableName(record.getTableName()); + } + updateRecordDataList.add(data.build()); + } + + V1UpdateRequest.Builder builder = V1UpdateRequest.builder() + .vaultId(config.getVaultId()) + .tableName(request.getTableName()) + .records(updateRecordDataList); + + FlowEnumUpdateType requestUpdateType = resolveUpdateType(request.getUpdateType()); + if (requestUpdateType != null) { + builder.updateType(requestUpdateType); + } + return builder.build(); + } + + // updateType is a String on the request; the legal values come from the wire enum itself + // so there is a single source of truth. Validations rejects anything that does not match. + // Record-level updateType isn't wired here: the generated V1UpdateRecordData has no + // updateType setter yet even though the proto declares one (needs a client regeneration). + private static FlowEnumUpdateType resolveUpdateType(String updateType) { + if (updateType == null) { + return null; + } + for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { + if (type.toString().equalsIgnoreCase(updateType)) { + return type; + } + } + return null; + } + // ── Bulk (batched/concurrent) request-body builders ────────────────────── // BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way. @@ -186,7 +347,7 @@ public static com.skyflow.generated.rest.resources.flowservice.requests.V1Insert return getInsertRequestBody(request, config); } - public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokenizeRequest request, String vaultId) { + public static V1FlowDetokenizeRequest getDetokenizeRequestBody(DetokenizeRequest request, String vaultId) { V1FlowDetokenizeRequest.Builder builder = V1FlowDetokenizeRequest.builder() .vaultId(vaultId) .tokens(request.getTokens()); @@ -203,6 +364,17 @@ public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokeniz return builder.build(); } + public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokenizeRequest request, String vaultId) { + return getDetokenizeRequestBody(request, vaultId); + } + + public static V1ExecuteQueryRequest getQueryRequestBody(QueryRequest request, String vaultId) { + return V1ExecuteQueryRequest.builder() + .vaultId(vaultId) + .query(request.getQuery()) + .build(); + } + public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest getBulkDeleteTokensRequestBody(BulkDeleteTokensRequest request, String vaultId) { return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() .vaultId(vaultId) @@ -743,6 +915,80 @@ private static String extractBatchErrorMessage(ApiClientApiException apiExceptio return apiException.getMessage(); } + // Unary counterpart of formatBulkInsertResponse: a single, unbatched call has no batch + // index or requestId to attach, so each record maps straight across with no offset. + public static InsertResponse formatInsertResponse(V1InsertResponse response) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + records.add(new InsertResponseRecord( + current.getTableName().orElse(null), + current.getSkyflowId().orElse(null), + Token.parseTokens(current.getTokens().orElse(null)), + current.getData().orElse(null), + current.getHashedData().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null))); + } + } + return new InsertResponse(records); + } + + // Update has no bulk/batched counterpart, so there is no index or requestId to attach here. + // The wire response reuses V1RecordResponseObject (the same shape as insert's), so the + // per-record mapping mirrors formatInsertResponse. + // Get has no bulk/batched counterpart, so there is no index or requestId to attach here. + // The wire response reuses V1RecordResponseObject (the same shape as insert's/update's), so + // the per-record mapping mirrors formatInsertResponse. + public static GetResponse formatGetResponse(V1GetResponse response) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + records.add(new GetResponseRecord( + current.getTableName().orElse(null), + current.getSkyflowId().orElse(null), + Token.parseTokens(current.getTokens().orElse(null)), + current.getData().orElse(null), + current.getHashedData().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null))); + } + } + return new GetResponse(records); + } + + // Delete has no bulk/batched counterpart, so there is no index or requestId to attach here. + // Unlike insert/update/get, the wire response (V1DeleteResponseObject) carries no data/tokens. + public static DeleteResponse formatDeleteResponse(V1DeleteResponse response) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1DeleteResponseObject current : response.getRecords().get()) { + records.add(new DeleteResponseRecord( + current.getSkyflowId().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null))); + } + } + return new DeleteResponse(records); + } + + public static UpdateResponse formatUpdateResponse(V1UpdateResponse response) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + records.add(new UpdateResponseRecord( + current.getTableName().orElse(null), + current.getSkyflowId().orElse(null), + Token.parseTokens(current.getTokens().orElse(null)), + current.getData().orElse(null), + current.getHashedData().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null))); + } + } + return new UpdateResponse(records); + } + public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse response, int batch, int batchSize, Map> headers) { BulkInsertResponse formattedResponse = null; List records = new ArrayList<>(); @@ -773,6 +1019,25 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo return formattedResponse; } + // Unary counterpart of formatBulkDetokenizeResponse: a single, unbatched call has no batch + // index or requestId to attach, so each record maps straight across with no offset. + public static DetokenizeResponse formatDetokenizeResponse(V1FlowDetokenizeResponse response) { + List records = new ArrayList<>(); + if (response != null && response.getResponse().isPresent()) { + for (V1FlowDetokenizeResponseObject current : response.getResponse().get()) { + DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(current.getMetadata().orElse(null)); + records.add(new DetokenizeResponseRecord( + current.getToken().orElse(null), + current.getValue().orElse(null), + current.getTokenGroupName().orElse(null), + metadata, + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null))); + } + } + return new DetokenizeResponse(records); + } + public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokenizeResponse response, int batch, int batchSize, Map> headers) { if (response != null && response.getResponse().isPresent()) { List record = response.getResponse().get(); @@ -802,6 +1067,23 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni return null; } + // Query has no batching/bulk counterpart, so there is no index or requestId to attach here. + public static QueryResponse formatQueryResponse(V1ExecuteQueryResponse response) { + List records = new ArrayList<>(); + List columns = null; + if (response != null) { + if (response.getRecords().isPresent()) { + for (V1ExecuteQueryRecordResponse record : response.getRecords().get()) { + records.add(new QueryResponseRecord(record.getData().orElse(null))); + } + } + if (response.getMetadata().isPresent()) { + columns = response.getMetadata().get().getColumns().orElse(null); + } + } + return new QueryResponse(records, columns); + } + public static BulkDeleteTokensResponse formatBulkDeleteTokensResponse( V1FlowDeleteTokenResponse response, com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batchRequest, diff --git a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java index f9208749..2c930ac0 100644 --- a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -411,6 +411,17 @@ public static void validateBulkDetokenizeRequest(BulkDetokenizeRequest request) ErrorMessage.TokensSizeExceedError, InterfaceName.DETOKENIZE); } + public static void validateDeleteRequest(DeleteRequest deleteRequest) throws SkyflowException { + if (deleteRequest == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.DELETE_REQUEST_NULL.getLog(), InterfaceName.DELETE_RECORDS.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DeleteRequestNull.getMessage()); + } + validateTableRequired(deleteRequest.getTable(), InterfaceName.DELETE_RECORDS); + validateIdsOrUniqueValues(deleteRequest.getIds(), deleteRequest.getUniqueValues(), InterfaceName.DELETE_RECORDS); + } + public static void validateBulkDeleteTokensRequest(BulkDeleteTokensRequest request) throws SkyflowException { if (request == null) { LogUtil.printErrorLog(Utils.parameterizedString( @@ -440,6 +451,290 @@ public static void validateBulkDeleteTokensRequest(BulkDeleteTokensRequest reque } } + public static void validateQueryRequest(QueryRequest queryRequest) throws SkyflowException { + if (queryRequest == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.QUERY_REQUEST_NULL.getLog(), InterfaceName.QUERY.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.QueryRequestNull.getMessage()); + } + String query = queryRequest.getQuery(); + if (query == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.QUERY_IS_REQUIRED.getLog(), InterfaceName.QUERY.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.QueryKeyError.getMessage()); + } else if (query.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_QUERY.getLog(), InterfaceName.QUERY.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyQuery.getMessage()); + } + } + + public static void validateUpdateRequest(UpdateRequest updateRequest) throws SkyflowException { + if (updateRequest == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.UPDATE_REQUEST_NULL.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpdateRequestNull.getMessage()); + } + String tableName = updateRequest.getTableName(); + if (tableName == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_IS_REQUIRED.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableKeyError.getMessage()); + } else if (tableName.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_TABLE_NAME.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTable.getMessage()); + } + + List records = updateRequest.getRecords(); + if (records == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.RECORDS_IS_REQUIRED.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.RecordsKeyError.getMessage()); + } else if (records.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_RECORDS.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRecords.getMessage()); + } + + for (UpdateRequestRecord record : records) { + if (record == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_RECORD.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.UpdateRecordNull.getMessage()); + } + String skyflowId = record.getSkyflowId(); + if (skyflowId == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.SKYFLOW_ID_IS_REQUIRED.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.RecordSkyflowIdKeyError.getMessage()); + } else if (skyflowId.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_SKYFLOW_ID.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptySkyflowIdInRecord.getMessage()); + } + + if (record.getData() != null) { + for (String key : record.getData().keySet()) { + if (key == null || key.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInRecords.getMessage()); + } else { + Object value = record.getData().get(key); + if (value == null || value.toString().trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), + InterfaceName.UPDATE.getName(), key + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); + } + } + } + } + + Map tokens = record.getTokens(); + if (tokens != null) { + if (tokens.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_TOKENS.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokens.getMessage()); + } + for (String key : tokens.keySet()) { + if (key == null || key.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_KEY_IN_TOKENS.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInTokens.getMessage()); + } + Object value = tokens.get(key); + if (value == null || value.toString().trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_TOKENS.getLog(), + InterfaceName.UPDATE.getName(), key + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokens.getMessage()); + } + } + } + } + + // updateType is a free-form String on the request, but only the wire enum's values reach + // the wire. Reject anything else here rather than silently dropping it during mapping. + String updateType = updateRequest.getUpdateType(); + if (updateType != null && !isKnownUpdateType(updateType)) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_UPSERT_UPDATE_TYPE.getLog(), InterfaceName.UPDATE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidUpsertUpdateType.getMessage()); + } + } + + public static void validateGetRequest(GetRequest getRequest) throws SkyflowException { + if (getRequest == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.GET_REQUEST_NULL.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.GetRequestNull.getMessage()); + } + + boolean hasSingleTableFields = hasText(getRequest.getTable()) + || (getRequest.getIds() != null && !getRequest.getIds().isEmpty()) + || (getRequest.getFields() != null && !getRequest.getFields().isEmpty()) + || (getRequest.getUniqueValues() != null && !getRequest.getUniqueValues().isEmpty()) + || (getRequest.getColumnRedactions() != null && !getRequest.getColumnRedactions().isEmpty()); + boolean hasRecords = getRequest.getRecords() != null && !getRequest.getRecords().isEmpty(); + + if (hasSingleTableFields && hasRecords) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.BOTH_SINGLE_TABLE_FIELDS_AND_RECORDS_PASSED.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.BothSingleTableFieldsAndRecordsSpecified.getMessage()); + } + + if (hasRecords) { + for (GetRequestRecord record : getRequest.getRecords()) { + if (record == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.NULL_GET_RECORD_REQUEST_OBJECT.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullGetRecordRequest.getMessage()); + } + validateTableRequired(record.getTable(), InterfaceName.GET); + validateIdsOrUniqueValues(record.getIds(), record.getUniqueValues(), InterfaceName.GET); + validateFields(record.getFields()); + validateColumnRedactions(record.getColumnRedactions()); + } + return; + } + + validateTableRequired(getRequest.getTable(), InterfaceName.GET); + validateIdsOrUniqueValues(getRequest.getIds(), getRequest.getUniqueValues(), InterfaceName.GET); + validateFields(getRequest.getFields()); + validateColumnRedactions(getRequest.getColumnRedactions()); + } + + private static void validateTableRequired(String table, InterfaceName interfaceName) throws SkyflowException { + if (table == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_IS_REQUIRED.getLog(), interfaceName.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TableKeyError.getMessage()); + } else if (table.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_TABLE_NAME.getLog(), interfaceName.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTable.getMessage()); + } + } + + // Either ids or uniqueValues selects the records within the table; specifying both, or + // neither, is invalid. + private static void validateIdsOrUniqueValues(List ids, List> uniqueValues, InterfaceName interfaceName) throws SkyflowException { + boolean hasIds = ids != null && !ids.isEmpty(); + boolean hasUniqueValues = uniqueValues != null && !uniqueValues.isEmpty(); + + if (hasIds && hasUniqueValues) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.BOTH_IDS_AND_UNIQUE_VALUES_PASSED.getLog(), interfaceName.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.BothIdsAndUniqueValuesSpecified.getMessage()); + } + if (!hasIds && !hasUniqueValues) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.NEITHER_IDS_NOR_UNIQUE_VALUES_PASSED.getLog(), interfaceName.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.IdsOrUniqueValuesKeyError.getMessage()); + } + + if (hasIds) { + for (int index = 0; index < ids.size(); index++) { + String id = ids.get(index); + if (id == null || id.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_ID_IN_IDS.getLog(), interfaceName.getName(), String.valueOf(index) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyIdInIds.getMessage()); + } + } + } else { + for (int index = 0; index < uniqueValues.size(); index++) { + Map uniqueValue = uniqueValues.get(index); + if (uniqueValue == null || uniqueValue.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_UNIQUE_VALUE_IN_UNIQUE_VALUES.getLog(), interfaceName.getName(), String.valueOf(index) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUniqueValueInUniqueValues.getMessage()); + } + } + } + } + + // fields is optional; when supplied, it must not be empty and no entry may be null/blank. + private static void validateFields(List fields) throws SkyflowException { + if (fields == null) { + return; + } + if (fields.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_FIELDS.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyFields.getMessage()); + } + for (int index = 0; index < fields.size(); index++) { + String field = fields.get(index); + if (field == null || field.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_FIELD_IN_FIELDS.getLog(), InterfaceName.GET.getName(), String.valueOf(index) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyFieldInFields.getMessage()); + } + } + } + + // columnRedactions is optional; when supplied, no entry may be null and each must carry a + // non-blank columnName and redaction. + private static void validateColumnRedactions(List columnRedactions) throws SkyflowException { + if (columnRedactions == null) { + return; + } + for (ColumnRedactions columnRedaction : columnRedactions) { + if (columnRedaction == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.NULL_COLUMN_REDACTION_OBJECT.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullColumnRedactions.getMessage()); + } + String columnName = columnRedaction.getColumnName(); + if (columnName == null || columnName.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.NULL_COLUMN_NAME_IN_COLUMN_REDACTION.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullColumnNameInColumnRedaction.getMessage()); + } + String redaction = columnRedaction.getRedaction(); + if (redaction == null || redaction.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_REDACTION_IN_COLUMN_REDACTION.getLog(), InterfaceName.GET.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullRedactionInColumnRedaction.getMessage()); + } + } + } + public static void validateBulkTokenizeRequest(BulkTokenizeRequest request) throws SkyflowException { if (request == null) { LogUtil.printErrorLog(Utils.parameterizedString( diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java index da0eca19..29eebea8 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -4,11 +4,11 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.function.Function; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -22,11 +22,19 @@ import com.skyflow.generated.rest.core.ApiClientHttpResponse; import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1UpdateRequest; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1InsertRecordData; -import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.generated.rest.types.V1InsertResponse; +import com.skyflow.generated.rest.types.V1DeleteResponse; +import com.skyflow.generated.rest.types.V1GetResponse; +import com.skyflow.generated.rest.types.V1UpdateResponse; +import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; import com.skyflow.logs.WarningLogs; @@ -35,28 +43,43 @@ import com.skyflow.utils.logger.LogUtil; import com.skyflow.utils.validations.Validations; import com.skyflow.vault.data.BulkDeleteTokensOptions; -import com.skyflow.vault.data.BulkTokenizeOptions; import com.skyflow.vault.data.BulkDeleteTokensRequest; -import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRequestRecord; -import com.skyflow.vault.data.BulkTokenizeResponseRecord; import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import com.skyflow.vault.data.BulkDetokenizeOptions; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; +import com.skyflow.vault.data.BulkTokenizeOptions; import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; -import com.skyflow.vault.data.BulkDetokenizeOptions; -import com.skyflow.vault.data.BulkInsertOptions; -import com.skyflow.vault.data.DeleteTokensOptions; -import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.DetokenizeOptions; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.InsertOptions; +import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.QueryOptions; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; import com.skyflow.vault.data.RequestContext; +import com.skyflow.vault.data.UpdateOptions; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateResponse; +import com.skyflow.vault.data.DeleteOptions; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.GetOptions; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; import com.skyflow.vault.data.RequestInterceptor; -import com.skyflow.vault.data.TokenizeOptions; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; @@ -92,6 +115,43 @@ private RequestOptions buildRequestOptions(RequestContext context) { return builder.build(); } + // ── Insert ──────────────────────────────────────────────────────────────── + // Unary counterpart of bulkInsert: sends every record in a single API call, with no + // batching or concurrency involved. + + public InsertResponse insert(InsertRequest insertRequest) throws SkyflowException { + return insert(insertRequest, null); + } + + public InsertResponse insert(InsertRequest insertRequest, InsertOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); + Validations.validateInsertRequest(insertRequest); + + setBearerToken(); + V1InsertRequest request = Utils.getInsertRequestBody(insertRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("INSERT", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getRecordsApi().withRawResponse().insert(request, buildRequestOptions(ctx)); + + InsertResponse formattedResponse = Utils.formatInsertResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + + // ── Bulk Insert ─────────────────────────────────────────────────────────── public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws SkyflowException { @@ -168,6 +228,43 @@ public CompletableFuture bulkInsertAsync(BulkInsertRequest i } } + // ── Detokenize ──────────────────────────────────────────────────────────── + // Unary counterpart of bulkDetokenize: sends every token in a single API call, with no + // batching or concurrency involved. + + public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest) throws SkyflowException { + return detokenize(detokenizeRequest, null); + } + + public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); + Validations.validateDetokenizeRequest(detokenizeRequest); + + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = + Utils.getDetokenizeRequestBody(detokenizeRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("DETOKENIZE", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getRecordsApi().withRawResponse().detokenize(request, buildRequestOptions(ctx)); + + DetokenizeResponse formattedResponse = Utils.formatDetokenizeResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + // ── Bulk Detokenize ─────────────────────────────────────────────────────── public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest) throws SkyflowException { @@ -250,6 +347,43 @@ public CompletableFuture bulkDetokenizeAsync(BulkDetoken } } + // ── Delete ──────────────────────────────────────────────────────────────── + // Deletes records by skyflowId or unique value, in a single API call. There is no + // bulk/batched counterpart of this operation. Distinct from deleteTokens/bulkDeleteTokens, + // which remove tokens only and leave the underlying record in place. + + public DeleteResponse delete(DeleteRequest deleteRequest) throws SkyflowException { + return delete(deleteRequest, null); + } + + public DeleteResponse delete(DeleteRequest deleteRequest, DeleteOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.DELETE_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATING_DELETE_REQUEST.getLog()); + Validations.validateDeleteRequest(deleteRequest); + + setBearerToken(); + V1DeleteRequest request = Utils.getDeleteRequestBody(deleteRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("DELETE", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getRecordsApi().withRawResponse().delete(request, buildRequestOptions(ctx)); + + DeleteResponse formattedResponse = Utils.formatDeleteResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.DELETE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + // ── Bulk Delete Tokens ──────────────────────────────────────────────────── public BulkDeleteTokensResponse bulkDeleteTokens(BulkDeleteTokensRequest deleteTokensRequest) throws SkyflowException { @@ -415,6 +549,111 @@ public CompletableFuture bulkTokenizeAsync(BulkTokenizeReq } } + // ── Query ───────────────────────────────────────────────────────────────── + // Runs a query in a single API call. There is no bulk/batched counterpart of this operation. + + public QueryResponse query(QueryRequest queryRequest) throws SkyflowException { + return query(queryRequest, null); + } + + public QueryResponse query(QueryRequest queryRequest, QueryOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.QUERY_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATING_QUERY_REQUEST.getLog()); + Validations.validateQueryRequest(queryRequest); + + setBearerToken(); + V1ExecuteQueryRequest request = Utils.getQueryRequestBody(queryRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("QUERY", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getQueryApi().withRawResponse().flowServiceExecuteQuery(request, buildRequestOptions(ctx)); + + QueryResponse formattedResponse = Utils.formatQueryResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.QUERY_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.QUERY_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.QUERY_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + + // ── Update ──────────────────────────────────────────────────────────────── + // Runs an update in a single API call. There is no bulk/batched counterpart of this operation. + + public UpdateResponse update(UpdateRequest updateRequest) throws SkyflowException { + return update(updateRequest, null); + } + + public UpdateResponse update(UpdateRequest updateRequest, UpdateOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.UPDATE_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_UPDATE_REQUEST.getLog()); + Validations.validateUpdateRequest(updateRequest); + + setBearerToken(); + V1UpdateRequest request = Utils.getUpdateRequestBody(updateRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("UPDATE", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getRecordsApi().withRawResponse().update(request, buildRequestOptions(ctx)); + + UpdateResponse formattedResponse = Utils.formatUpdateResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.UPDATE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.UPDATE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.UPDATE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + + // ── Get ─────────────────────────────────────────────────────────────────── + // Runs a get in a single API call. There is no bulk/batched counterpart of this operation. + + public GetResponse get(GetRequest getRequest) throws SkyflowException { + return get(getRequest, null); + } + + public GetResponse get(GetRequest getRequest, GetOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.GET_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_GET_REQUEST.getLog()); + Validations.validateGetRequest(getRequest); + + setBearerToken(); + V1GetRequest request = Utils.getGetRequestBody(getRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + RequestContext ctx = new RequestContext("GET", 0, 1); + if (interceptor != null) interceptor.intercept(ctx); + + ApiClientHttpResponse response = + this.getRecordsApi().withRawResponse().get(request, buildRequestOptions(ctx)); + + GetResponse formattedResponse = Utils.formatGetResponse(response.body()); + LogUtil.printInfoLog(InfoLogs.GET_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ApiClientException e) { + LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e); + } + } + // ── Bulk private helpers ────────────────────────────────────────────────── private BulkDeleteTokensResponse processBulkDeleteTokensSync( diff --git a/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedactions.java b/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedactions.java new file mode 100644 index 00000000..68d35171 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/ColumnRedactions.java @@ -0,0 +1,40 @@ +package com.skyflow.vault.data; + +public class ColumnRedactions { + private final ColumnRedactionsBuilder builder; + + private ColumnRedactions(ColumnRedactionsBuilder builder) { + this.builder = builder; + } + + public String getColumnName() { + return this.builder.columnName; + } + + public String getRedaction() { + return this.builder.redaction; + } + + public static ColumnRedactionsBuilder builder() { + return new ColumnRedactionsBuilder(); + } + + public static final class ColumnRedactionsBuilder { + private String columnName; + private String redaction; + + public ColumnRedactionsBuilder columnName(String columnName) { + this.columnName = columnName; + return this; + } + + public ColumnRedactionsBuilder redaction(String redaction) { + this.redaction = redaction; + return this; + } + + public ColumnRedactions build() { + return new ColumnRedactions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteOptions.java new file mode 100644 index 00000000..7a6a075e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class DeleteOptions { + private final RequestInterceptor interceptor; + + protected DeleteOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public DeleteOptions build() { + return new DeleteOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java new file mode 100644 index 00000000..47e75b87 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java @@ -0,0 +1,59 @@ +package com.skyflow.vault.data; + +import java.util.List; +import java.util.Map; + +// Deletes records by skyflowId or unique value. Distinct from DeleteTokensRequest, which +// removes tokens only and leaves the underlying record in place. +public class DeleteRequest { + private final DeleteRequestBuilder builder; + + protected DeleteRequest(DeleteRequestBuilder builder) { + this.builder = builder; + } + + public String getTable() { + return this.builder.table; + } + + /** Either this or {@code uniqueValues} is required; specifying both fails validation. */ + public List getIds() { + return this.builder.ids; + } + + public List> getUniqueValues() { + return this.builder.uniqueValues; + } + + public static DeleteRequestBuilder builder() { + return new DeleteRequestBuilder(); + } + + public static final class DeleteRequestBuilder { + private String table; + private List ids; + private List> uniqueValues; + + protected DeleteRequestBuilder() { + } + + public DeleteRequestBuilder table(String table) { + this.table = table; + return this; + } + + public DeleteRequestBuilder ids(List ids) { + this.ids = ids; + return this; + } + + public DeleteRequestBuilder uniqueValues(List> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public DeleteRequest build() { + return new DeleteRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java new file mode 100644 index 00000000..70a00eda --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class DeleteResponse { + private final List records; + + public DeleteResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java new file mode 100644 index 00000000..64c6f3e0 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java @@ -0,0 +1,37 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +// The wire response (V1DeleteResponseObject) is simpler than insert/update/get's — just the +// deleted record's skyflowId plus a partial error, no data/tokens/hashedData — so this does not +// extend InsertResponseRecord. +public class DeleteResponseRecord { + private final String skyflowId; + private final Integer httpCode; + private final String error; + + public DeleteResponseRecord(String skyflowId, Integer httpCode, String error) { + this.skyflowId = skyflowId; + this.httpCode = httpCode; + this.error = error; + } + + public String getSkyflowId() { + return skyflowId; + } + + public Integer getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java new file mode 100644 index 00000000..681703a6 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class GetOptions { + private final RequestInterceptor interceptor; + + protected GetOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public GetOptions build() { + return new GetOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java new file mode 100644 index 00000000..167e918e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetRequest.java @@ -0,0 +1,101 @@ +package com.skyflow.vault.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class GetRequest extends BaseGetRequest { + private final GetRequestBuilder builder; + + protected GetRequest(GetRequestBuilder builder) { + super(builder); + this.builder = builder; + } + + public List getColumnRedactions() { + return this.builder.columnRedactions; + } + + /** Either this or {@code ids} is required in single-table mode; specifying both fails validation. */ + public List> getUniqueValues() { + return this.builder.uniqueValues; + } + + public Integer getLimit() { + return this.builder.limit; + } + + public Integer getOffset() { + return this.builder.offset; + } + + /** + * Multi-table lookup: mutually exclusive with the single-table fields above + * (table/ids/fields/uniqueValues/columnRedactions) — specify one or the other, not both. + */ + public List getRecords() { + return this.builder.records; + } + + public static GetRequestBuilder builder() { + return new GetRequestBuilder(); + } + + public static final class GetRequestBuilder extends BaseGetRequestBuilder { + private List columnRedactions; + private List> uniqueValues; + private Integer limit; + private Integer offset; + private List records; + + private GetRequestBuilder() { + } + + @Override + public GetRequestBuilder table(String table) { + super.table(table); + return this; + } + + @Override + public GetRequestBuilder ids(ArrayList ids) { + super.ids(ids); + return this; + } + + @Override + public GetRequestBuilder fields(ArrayList fields) { + super.fields(fields); + return this; + } + + public GetRequestBuilder columnRedactions(List columnRedactions) { + this.columnRedactions = columnRedactions; + return this; + } + + public GetRequestBuilder uniqueValues(List> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public GetRequestBuilder limit(Integer limit) { + this.limit = limit; + return this; + } + + public GetRequestBuilder offset(Integer offset) { + this.offset = offset; + return this; + } + + public GetRequestBuilder records(List records) { + this.records = records; + return this; + } + + public GetRequest build() { + return new GetRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/GetRequestRecord.java new file mode 100644 index 00000000..78cfe500 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetRequestRecord.java @@ -0,0 +1,79 @@ +package com.skyflow.vault.data; + +import java.util.List; +import java.util.Map; + +// One table's worth of lookup fields inside GetRequest#getRecords() (the multi-table mode). +// Mirrors GetRequest's own single-table fields, minus limit/offset, which only apply once for +// the whole call. +public class GetRequestRecord { + private final GetRequestRecordBuilder builder; + + protected GetRequestRecord(GetRequestRecordBuilder builder) { + this.builder = builder; + } + + public String getTable() { + return this.builder.table; + } + + public List getIds() { + return this.builder.ids; + } + + public List getFields() { + return this.builder.fields; + } + + public List getColumnRedactions() { + return this.builder.columnRedactions; + } + + public List> getUniqueValues() { + return this.builder.uniqueValues; + } + + public static GetRequestRecordBuilder builder() { + return new GetRequestRecordBuilder(); + } + + public static final class GetRequestRecordBuilder { + private String table; + private List ids; + private List fields; + private List columnRedactions; + private List> uniqueValues; + + protected GetRequestRecordBuilder() { + } + + public GetRequestRecordBuilder table(String table) { + this.table = table; + return this; + } + + public GetRequestRecordBuilder ids(List ids) { + this.ids = ids; + return this; + } + + public GetRequestRecordBuilder fields(List fields) { + this.fields = fields; + return this; + } + + public GetRequestRecordBuilder columnRedactions(List columnRedactions) { + this.columnRedactions = columnRedactions; + return this; + } + + public GetRequestRecordBuilder uniqueValues(List> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public GetRequestRecord build() { + return new GetRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java new file mode 100644 index 00000000..eb247ea7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetResponse.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class GetResponse { + private final List records; + + public GetResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java new file mode 100644 index 00000000..91cd3704 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java @@ -0,0 +1,15 @@ +package com.skyflow.vault.data; + +import java.util.List; +import java.util.Map; + +// The wire shape is identical to InsertResponseRecord (the vault returns the same +// V1RecordResponseObject for get as it does for insert/update), kept as its own type so a +// get response doesn't expose an "Insert*" class name. +public class GetResponseRecord extends InsertResponseRecord { + public GetResponseRecord(String tableName, String skyflowId, Map> tokens, + Map data, Map hashedData, + int httpCode, String error) { + super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java new file mode 100644 index 00000000..fb472bd7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class QueryOptions { + private final RequestInterceptor interceptor; + + protected QueryOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public QueryOptions build() { + return new QueryOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java new file mode 100644 index 00000000..871375cb --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryRequest.java @@ -0,0 +1,28 @@ +package com.skyflow.vault.data; + +public class QueryRequest extends BaseQueryRequest { + + protected QueryRequest(QueryRequestBuilder builder) { + super(builder); + } + + public static QueryRequestBuilder builder() { + return new QueryRequestBuilder(); + } + + public static final class QueryRequestBuilder extends BaseQueryRequestBuilder { + + private QueryRequestBuilder() { + } + + @Override + public QueryRequestBuilder query(String query) { + super.query(query); + return this; + } + + public QueryRequest build() { + return new QueryRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java new file mode 100644 index 00000000..132cf627 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class QueryResponse { + private final List records; + private final List columns; + + public QueryResponse(List records, List columns) { + this.records = records; + this.columns = columns; + } + + public List getRecords() { + return records; + } + + /** The return columns for the query, when the vault reports them. */ + public List getColumns() { + return columns; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/QueryResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponseRecord.java new file mode 100644 index 00000000..23aa89e5 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponseRecord.java @@ -0,0 +1,26 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.Map; + +// A single row returned by a query, as the free-form column/value map the vault sends back. +// The query API has no notion of tokens, so unlike insert/detokenize there is no typed Token data here. +public class QueryResponseRecord { + private final Map data; + + public QueryResponseRecord(Map data) { + this.data = data; + } + + public Map getData() { + return data; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpdateOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateOptions.java new file mode 100644 index 00000000..d4f81545 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class UpdateOptions { + private final RequestInterceptor interceptor; + + protected UpdateOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public UpdateOptions build() { + return new UpdateOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java new file mode 100644 index 00000000..490d4a3a --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java @@ -0,0 +1,56 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class UpdateRequest { + private final UpdateRequestBuilder builder; + + protected UpdateRequest(UpdateRequestBuilder builder) { + this.builder = builder; + } + + public String getTableName() { + return this.builder.tableName; + } + + public List getRecords() { + return this.builder.records; + } + + /** "UPDATE" or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". */ + public String getUpdateType() { + return this.builder.updateType; + } + + public static UpdateRequestBuilder builder() { + return new UpdateRequestBuilder(); + } + + public static class UpdateRequestBuilder { + private String tableName; + private List records; + private String updateType; + + protected UpdateRequestBuilder() { + } + + public UpdateRequestBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public UpdateRequestBuilder records(List records) { + this.records = records; + return this; + } + + public UpdateRequestBuilder updateType(String updateType) { + this.updateType = updateType; + return this; + } + + public UpdateRequest build() { + return new UpdateRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequestRecord.java new file mode 100644 index 00000000..61d72b80 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequestRecord.java @@ -0,0 +1,66 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class UpdateRequestRecord { + private final UpdateRequestRecordBuilder builder; + + protected UpdateRequestRecord(UpdateRequestRecordBuilder builder) { + this.builder = builder; + } + + public String getSkyflowId() { + return this.builder.skyflowId; + } + + public Map getData() { + return this.builder.data; + } + + public Map getTokens() { + return this.builder.tokens; + } + + /** Overrides UpdateRequest#getTableName() for this record only. */ + public String getTableName() { + return this.builder.tableName; + } + + public static UpdateRequestRecordBuilder builder() { + return new UpdateRequestRecordBuilder(); + } + + public static class UpdateRequestRecordBuilder { + private String skyflowId; + private Map data; + private Map tokens; + private String tableName; + + protected UpdateRequestRecordBuilder() { + } + + public UpdateRequestRecordBuilder skyflowId(String skyflowId) { + this.skyflowId = skyflowId; + return this; + } + + public UpdateRequestRecordBuilder data(Map data) { + this.data = data; + return this; + } + + public UpdateRequestRecordBuilder tokens(Map tokens) { + this.tokens = tokens; + return this; + } + + public UpdateRequestRecordBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public UpdateRequestRecord build() { + return new UpdateRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java new file mode 100644 index 00000000..20248dfa --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class UpdateResponse { + private final List records; + + public UpdateResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java new file mode 100644 index 00000000..10777287 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java @@ -0,0 +1,15 @@ +package com.skyflow.vault.data; + +import java.util.List; +import java.util.Map; + +// The wire shape is identical to InsertResponseRecord (the vault returns the same +// V1RecordResponseObject for both insert and update), kept as its own type so an update +// response doesn't expose an "Insert*" class name. +public class UpdateResponseRecord extends InsertResponseRecord { + public UpdateResponseRecord(String tableName, String skyflowId, Map> tokens, + Map data, Map hashedData, + int httpCode, String error) { + super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); + } +} From bfb90a05ae28cd3cb8413566a231e36440279c0f Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 8 Sep 2026 13:06:34 +0530 Subject: [PATCH 2/2] SK-3131 Added unary operations samples --- .../java/com/example/vault/DeleteExample.java | 72 ++ .../com/example/vault/DetokenizeExample.java | 76 +++ .../java/com/example/vault/GetExample.java | 83 +++ .../java/com/example/vault/InsertExample.java | 86 +++ .../java/com/example/vault/QueryExample.java | 56 ++ .../java/com/example/vault/UpdateExample.java | 80 +++ .../java/com/skyflow/utils/UtilsTests.java | 346 +++++++++- .../utils/validations/ValidationsTests.java | 632 +++++++++++++++++- .../controller/VaultControllerTests.java | 296 +++++++- 9 files changed, 1715 insertions(+), 12 deletions(-) create mode 100644 flowvault/samples/src/main/java/com/example/vault/DeleteExample.java create mode 100644 flowvault/samples/src/main/java/com/example/vault/DetokenizeExample.java create mode 100644 flowvault/samples/src/main/java/com/example/vault/GetExample.java create mode 100644 flowvault/samples/src/main/java/com/example/vault/InsertExample.java create mode 100644 flowvault/samples/src/main/java/com/example/vault/QueryExample.java create mode 100644 flowvault/samples/src/main/java/com/example/vault/UpdateExample.java diff --git a/flowvault/samples/src/main/java/com/example/vault/DeleteExample.java b/flowvault/samples/src/main/java/com/example/vault/DeleteExample.java new file mode 100644 index 00000000..224e0ac9 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/DeleteExample.java @@ -0,0 +1,72 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.DeleteResponseRecord; + +import java.util.ArrayList; +import java.util.List; + +/** + * This sample demonstrates the Skyflow Java SDK's unary delete operation — deleting records by + * skyflowId or unique value. This makes exactly one API call per invocation: there is no internal + * batching or concurrency to configure. + * + * Distinct from deleteTokens/bulkDeleteTokens, which remove tokens only and leave the underlying + * record in place. + */ +public class DeleteExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the skyflow IDs to delete. + // Either ids or uniqueValues is required; specifying both fails validation. + List ids = new ArrayList<>(); + ids.add(""); + + // Step 5: Build and execute the delete request + DeleteRequest request = DeleteRequest.builder() + .table("") + .ids(ids) + .build(); + + DeleteResponse response = skyflowClient.vault().delete(request); + + // Step 6: Read the outcome. A record succeeded when its error is null. + for (DeleteResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("delete: skyflowId=%s removed%n", record.getSkyflowId()); + } else { + System.out.printf("delete failed (%d): %s%n", record.getHttpCode(), record.getError()); + } + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in delete operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/DetokenizeExample.java b/flowvault/samples/src/main/java/com/example/vault/DetokenizeExample.java new file mode 100644 index 00000000..62068da7 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/DetokenizeExample.java @@ -0,0 +1,76 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.DetokenizeResponseRecord; +import com.skyflow.vault.data.TokenGroupRedactions; + +import java.util.ArrayList; +import java.util.List; + +/** + * This sample demonstrates the Skyflow Java SDK's unary detokenize operation. Unlike + * bulkDetokenize, this makes exactly one API call per invocation: there is no internal batching + * or concurrency to configure. + */ +public class DetokenizeExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the tokens to detokenize and any per-group redactions + List tokens = new ArrayList<>(); + tokens.add(""); + + List tokenGroupRedactions = new ArrayList<>(); + tokenGroupRedactions.add(TokenGroupRedactions.builder() + .tokenGroupName("") + .redaction("PLAIN_TEXT") + .build()); + + // Step 5: Build and execute the detokenize request + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(tokenGroupRedactions) + .build(); + + DetokenizeResponse response = skyflowClient.vault().detokenize(request); + + // Step 6: Read the outcome. A record succeeded when its error is null. + for (DetokenizeResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("detokenize: %s -> %s (tableName=%s)%n", + record.getToken(), record.getValue(), record.getMetadata().getTableName()); + } else { + System.out.printf("detokenize failed (%d): %s%n", record.getHttpCode(), record.getError()); + } + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in detokenize operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/GetExample.java b/flowvault/samples/src/main/java/com/example/vault/GetExample.java new file mode 100644 index 00000000..ad7d86e2 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/GetExample.java @@ -0,0 +1,83 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.GetResponseRecord; +import com.skyflow.vault.data.Token; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This sample demonstrates the Skyflow Java SDK's unary get operation. This makes exactly one + * API call per invocation: there is no internal batching or concurrency to configure. + * + * A GetRequest works in one of two mutually exclusive modes: single-table (as shown here — a + * table plus ids or uniqueValues) or multi-table, via GetRequest#getRecords() (a list of + * GetRequestRecord, each specifying its own table). + */ +public class GetExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the skyflow IDs to fetch and any column redactions + ArrayList ids = new ArrayList<>(); + ids.add(""); + + List columnRedactions = new ArrayList<>(); + columnRedactions.add(ColumnRedactions.builder() + .columnName("") + .redaction("PLAIN_TEXT") + .build()); + + // Step 5: Build and execute the get request + GetRequest request = GetRequest.builder() + .table("") + .ids(ids) + .columnRedactions(columnRedactions) + .build(); + + GetResponse response = skyflowClient.vault().get(request); + + // Step 6: Read the fetched records + for (GetResponseRecord record : response.getRecords()) { + System.out.printf("get: %s -> skyflowId=%s%n", record.getTableName(), record.getSkyflowId()); + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in get operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/InsertExample.java b/flowvault/samples/src/main/java/com/example/vault/InsertExample.java new file mode 100644 index 00000000..3afcf813 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/InsertExample.java @@ -0,0 +1,86 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.InsertResponseRecord; +import com.skyflow.vault.data.Token; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This sample demonstrates the Skyflow Java SDK's unary insert operation. Unlike bulkInsert, + * this makes exactly one API call per invocation: there is no internal batching or concurrency + * to configure. + */ +public class InsertExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the record to insert + Map data = new HashMap<>(); + data.put("", ""); + + InsertRequestRecord record = InsertRequestRecord.builder() + .data(data) + .build(); + + List records = new ArrayList<>(); + records.add(record); + + // Step 5: Build and execute the insert request + InsertRequest request = InsertRequest.builder() + .tableName("") + .records(records) + .build(); + + InsertResponse response = skyflowClient.vault().insert(request); + + // Step 6: Read the outcome. A record succeeded when its error is null. + for (InsertResponseRecord insertedRecord : response.getRecords()) { + if (insertedRecord.getError() == null) { + System.out.printf("insert: %s -> skyflowId=%s%n", + insertedRecord.getTableName(), insertedRecord.getSkyflowId()); + for (Map.Entry> column : insertedRecord.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("insert failed (%d): %s%n", insertedRecord.getHttpCode(), insertedRecord.getError()); + } + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in insert operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/QueryExample.java b/flowvault/samples/src/main/java/com/example/vault/QueryExample.java new file mode 100644 index 00000000..8879a250 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/QueryExample.java @@ -0,0 +1,56 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.QueryResponseRecord; + +/** + * This sample demonstrates the Skyflow Java SDK's query operation. There is no bulk/batched + * counterpart of this operation — a single call runs the query as-is. + */ +public class QueryExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Build and execute the query request + QueryRequest request = QueryRequest.builder() + .query("SELECT * FROM LIMIT 1") + .build(); + + QueryResponse response = skyflowClient.vault().query(request); + + // Step 5: Read the returned rows and the reported columns + for (QueryResponseRecord record : response.getRecords()) { + System.out.println("query row: " + record.getData()); + } + System.out.println("columns: " + response.getColumns()); + } catch (SkyflowException e) { + // Step 6: Handle any errors that occur during the process + System.err.println("Error in query operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/UpdateExample.java b/flowvault/samples/src/main/java/com/example/vault/UpdateExample.java new file mode 100644 index 00000000..a74907da --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/UpdateExample.java @@ -0,0 +1,80 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; +import com.skyflow.vault.data.UpdateResponse; +import com.skyflow.vault.data.UpdateResponseRecord; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This sample demonstrates the Skyflow Java SDK's unary update operation. This makes exactly one + * API call per invocation: there is no internal batching or concurrency to configure. + */ +public class UpdateExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file +// String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the record to update, identified by its skyflow ID + Map data = new HashMap<>(); + data.put("", ""); + + UpdateRequestRecord updateRecord = UpdateRequestRecord.builder() + .skyflowId("") + .data(data) + .build(); + + List records = new ArrayList<>(); + records.add(updateRecord); + + // Step 5: Build and execute the update request. + // updateType accepts "UPDATE" (default) or "REPLACE". + UpdateRequest request = UpdateRequest.builder() + .tableName("") + .records(records) + .updateType("REPLACE") + .build(); + + UpdateResponse response = skyflowClient.vault().update(request); + + // Step 6: Read the outcome. A record succeeded when its error is null. + for (UpdateResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("update: %s -> skyflowId=%s%n", record.getTableName(), record.getSkyflowId()); + } else { + System.out.printf("update failed (%d): %s%n", record.getHttpCode(), record.getError()); + } + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in update operation:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 3d0ab81d..74325396 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -6,20 +6,31 @@ import com.skyflow.enums.Env; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1UpdateRequest; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.V1DeleteResponse; +import com.skyflow.generated.rest.types.V1DeleteResponseObject; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponseMetadata; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1GetResponse; import com.skyflow.generated.rest.types.V1InsertRecordData; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1UpdateResponse; import com.skyflow.vault.data.BulkDeleteTokensRequest; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; @@ -33,14 +44,28 @@ import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; +import com.skyflow.vault.data.GetResponse; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.BulkTokenizeResponseRecord; import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.TokenizeRequest; import com.skyflow.vault.data.TokenizeResponse; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; +import com.skyflow.vault.data.UpdateResponse; import com.skyflow.vault.data.UpsertOptions; import org.junit.After; import org.junit.Assert; @@ -326,9 +351,321 @@ public void testGetInsertRequestBody_withUpsertAtRecordLevel() { Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); } - // Tests for buildInsertResponse / getDetokenizeRequestBody / buildDetokenizeResponse / - // getTokenizeRequestBody / buildTokenizeResponse / getDeleteTokensRequestBody / - // buildDeleteTokensResponse were removed: those unary Utils helpers no longer exist (bulk-only module). + // ── formatInsertResponse (unary) ────────────────────────────────────────── + + @Test + public void testFormatInsertResponse_successRecord() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + InsertResponse formatted = Utils.formatInsertResponse(response); + + Assert.assertEquals(1, formatted.getRecords().size()); + Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); + Assert.assertEquals("sky-id-1", formatted.getRecords().get(0).getSkyflowId()); + Assert.assertEquals(200, formatted.getRecords().get(0).getHttpCode()); + Assert.assertNull(formatted.getRecords().get(0).getError()); + } + + @Test + public void testFormatInsertResponse_errorRecordDefaultsHttpCode500() { + V1RecordResponseObject record = V1RecordResponseObject.builder().error("failed").build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + InsertResponse formatted = Utils.formatInsertResponse(response); + + Assert.assertEquals("failed", formatted.getRecords().get(0).getError()); + Assert.assertEquals(500, formatted.getRecords().get(0).getHttpCode()); + } + + @Test + public void testFormatInsertResponse_nullResponseReturnsEmptyRecords() { + InsertResponse formatted = Utils.formatInsertResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + // ── getDetokenizeRequestBody / formatDetokenizeResponse (unary) ─────────── + + @Test + public void testGetDetokenizeRequestBody_buildsCorrectRequest() { + List tokens = Collections.singletonList("tok-1"); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(tokens).build(); + + V1FlowDetokenizeRequest body = Utils.getDetokenizeRequestBody(request, "vault123"); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals(tokens, body.getTokens().get()); + Assert.assertFalse(body.getTokenGroupRedactions().isPresent()); + } + + @Test + public void testGetDetokenizeRequestBody_withTokenGroupRedactions() { + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("tok-1")) + .tokenGroupRedactions(Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build())) + .build(); + + V1FlowDetokenizeRequest body = Utils.getDetokenizeRequestBody(request, "vault123"); + + Assert.assertEquals(1, body.getTokenGroupRedactions().get().size()); + Assert.assertEquals("group1", body.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", body.getTokenGroupRedactions().get().get(0).getRedaction().get()); + } + + @Test + public void testFormatDetokenizeResponse_successRecord() { + Map metadata = new HashMap<>(); + metadata.put("skyflowId", "sky-1"); + metadata.put("tableName", "table1"); + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("tok-1").value("john@example.com").tokenGroupName("group1").metadata(metadata).build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)).build(); + + DetokenizeResponse formatted = Utils.formatDetokenizeResponse(response); + + Assert.assertEquals(1, formatted.getRecords().size()); + Assert.assertEquals("tok-1", formatted.getRecords().get(0).getToken()); + Assert.assertEquals("john@example.com", formatted.getRecords().get(0).getValue()); + Assert.assertEquals("sky-1", formatted.getRecords().get(0).getMetadata().getSkyflowId()); + Assert.assertEquals("table1", formatted.getRecords().get(0).getMetadata().getTableName()); + Assert.assertEquals(200, formatted.getRecords().get(0).getHttpCode()); + } + + @Test + public void testFormatDetokenizeResponse_nullResponseReturnsEmptyRecords() { + DetokenizeResponse formatted = Utils.formatDetokenizeResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + // ── getUpdateRequestBody / formatUpdateResponse ─────────────────────────── + + @Test + public void testGetUpdateRequestBody_buildsCorrectRequest() { + Map data = new HashMap<>(); + data.put("name", "jane"); + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky-1").data(data).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1UpdateRequest body = Utils.getUpdateRequestBody(request, config); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertEquals("sky-1", body.getRecords().get().get(0).getSkyflowId().get()); + Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); + Assert.assertFalse(body.getUpdateType().isPresent()); + } + + @Test + public void testGetUpdateRequestBody_withRequestLevelUpdateTypeAndRecordTableName() { + UpdateRequestRecord record = UpdateRequestRecord.builder() + .skyflowId("sky-1").data(new HashMap<>()).tableName("table2").build(); + UpdateRequest request = UpdateRequest.builder() + .tableName("table1").records(Collections.singletonList(record)).updateType("REPLACE").build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1UpdateRequest body = Utils.getUpdateRequestBody(request, config); + + Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getUpdateType().get()); + Assert.assertEquals("table2", body.getRecords().get().get(0).getTableName().get()); + } + + @Test + public void testGetUpdateRequestBody_withTokens() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + UpdateRequestRecord record = UpdateRequestRecord.builder() + .skyflowId("sky-1").data(new HashMap<>()).tokens(tokens).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1UpdateRequest body = Utils.getUpdateRequestBody(request, config); + + Assert.assertEquals(tokens, body.getRecords().get().get(0).getTokens().get()); + } + + @Test + public void testFormatUpdateResponse_successRecord() { + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-1").build(); + V1UpdateResponse response = V1UpdateResponse.builder().records(Collections.singletonList(record)).build(); + + UpdateResponse formatted = Utils.formatUpdateResponse(response); + + Assert.assertEquals(1, formatted.getRecords().size()); + Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); + Assert.assertEquals("sky-1", formatted.getRecords().get(0).getSkyflowId()); + Assert.assertEquals(200, formatted.getRecords().get(0).getHttpCode()); + } + + @Test + public void testFormatUpdateResponse_nullResponseReturnsEmptyRecords() { + UpdateResponse formatted = Utils.formatUpdateResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + // ── getGetRequestBody / formatGetResponse ───────────────────────────────── + + @Test + public void testGetGetRequestBody_singleTableMode() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .fields(new ArrayList<>(Collections.singletonList("name"))) + .columnRedactions(Collections.singletonList( + ColumnRedactions.builder().columnName("email").redaction("MASKED").build())) + .limit(10) + .offset(5) + .build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1GetRequest body = Utils.getGetRequestBody(request, config); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertEquals(Collections.singletonList("id1"), body.getSkyflowIDs().get()); + Assert.assertEquals(Collections.singletonList("name"), body.getColumns().get()); + Assert.assertEquals("email", body.getColumnRedactions().get().get(0).getColumnName().get()); + Assert.assertEquals(Integer.valueOf(10), body.getLimit().get()); + Assert.assertEquals(Integer.valueOf(5), body.getOffset().get()); + Assert.assertFalse(body.getRecords().isPresent()); + } + + @Test + public void testGetGetRequestBody_multiTableModeIgnoresSingleTableFields() { + GetRequestRecord nested = GetRequestRecord.builder() + .table("table2").ids(Collections.singletonList("id2")).build(); + GetRequest request = GetRequest.builder().records(Collections.singletonList(nested)).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1GetRequest body = Utils.getGetRequestBody(request, config); + + Assert.assertTrue(body.getRecords().isPresent()); + Assert.assertEquals(1, body.getRecords().get().size()); + Assert.assertEquals("table2", body.getRecords().get().get(0).getTableName().get()); + Assert.assertEquals(Collections.singletonList("id2"), body.getRecords().get().get(0).getSkyflowIDs().get()); + Assert.assertFalse(body.getTableName().isPresent()); + } + + @Test + public void testFormatGetResponse_successRecord() { + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-1").build(); + V1GetResponse response = V1GetResponse.builder().records(Collections.singletonList(record)).build(); + + GetResponse formatted = Utils.formatGetResponse(response); + + Assert.assertEquals(1, formatted.getRecords().size()); + Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); + Assert.assertEquals("sky-1", formatted.getRecords().get(0).getSkyflowId()); + } + + @Test + public void testFormatGetResponse_nullResponseReturnsEmptyRecords() { + GetResponse formatted = Utils.formatGetResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + // ── getDeleteRequestBody / formatDeleteResponse ─────────────────────────── + + @Test + public void testGetDeleteRequestBody_withIds() { + DeleteRequest request = DeleteRequest.builder().table("table1").ids(Collections.singletonList("id1")).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1DeleteRequest body = Utils.getDeleteRequestBody(request, config); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertEquals(Collections.singletonList("id1"), body.getSkyflowIDs().get()); + Assert.assertFalse(body.getUniqueValues().isPresent()); + } + + @Test + public void testGetDeleteRequestBody_withUniqueValues() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + DeleteRequest request = DeleteRequest.builder() + .table("table1").uniqueValues(Collections.singletonList(uniqueValue)).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1DeleteRequest body = Utils.getDeleteRequestBody(request, config); + + Assert.assertEquals(uniqueValue, body.getUniqueValues().get().get(0).getData().get()); + Assert.assertFalse(body.getSkyflowIDs().isPresent()); + } + + @Test + public void testFormatDeleteResponse_successAndErrorRecords() { + V1DeleteResponseObject success = V1DeleteResponseObject.builder().skyflowId("sky-1").httpCode(200).build(); + V1DeleteResponseObject failure = V1DeleteResponseObject.builder().error("not found").httpCode(404).build(); + V1DeleteResponse response = V1DeleteResponse.builder().records(Arrays.asList(success, failure)).build(); + + DeleteResponse formatted = Utils.formatDeleteResponse(response); + + Assert.assertEquals(2, formatted.getRecords().size()); + Assert.assertEquals("sky-1", formatted.getRecords().get(0).getSkyflowId()); + Assert.assertEquals(Integer.valueOf(200), formatted.getRecords().get(0).getHttpCode()); + Assert.assertNull(formatted.getRecords().get(0).getError()); + Assert.assertEquals("not found", formatted.getRecords().get(1).getError()); + Assert.assertEquals(Integer.valueOf(404), formatted.getRecords().get(1).getHttpCode()); + } + + @Test + public void testFormatDeleteResponse_nullResponseReturnsEmptyRecords() { + DeleteResponse formatted = Utils.formatDeleteResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + // ── getQueryRequestBody / formatQueryResponse ───────────────────────────── + + @Test + public void testGetQueryRequestBody_buildsCorrectRequest() { + QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + + V1ExecuteQueryRequest body = Utils.getQueryRequestBody(request, "vault123"); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals("SELECT * FROM table1", body.getQuery().get()); + } + + @Test + public void testFormatQueryResponse_withRecordsAndMetadata() { + Map row = new HashMap<>(); + row.put("name", "john"); + V1ExecuteQueryRecordResponse record = V1ExecuteQueryRecordResponse.builder().data(row).build(); + V1ExecuteQueryResponseMetadata metadata = V1ExecuteQueryResponseMetadata.builder() + .columns(Collections.singletonList("name")).build(); + V1ExecuteQueryResponse response = V1ExecuteQueryResponse.builder() + .records(Collections.singletonList(record)).metadata(metadata).build(); + + QueryResponse formatted = Utils.formatQueryResponse(response); + + Assert.assertEquals(1, formatted.getRecords().size()); + Assert.assertEquals(row, formatted.getRecords().get(0).getData()); + Assert.assertEquals(Collections.singletonList("name"), formatted.getColumns()); + } + + @Test + public void testFormatQueryResponse_nullResponseReturnsEmptyRecordsAndNullColumns() { + QueryResponse formatted = Utils.formatQueryResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + Assert.assertNull(formatted.getColumns()); + } // ── getBulkInsertRequestBody (bulk overload) ────────────────────────────── @@ -2031,9 +2368,6 @@ public void testFormatBulkTokenizeResponse_nullResponseReturnsNull() { null, tokenizeBatch("value1", "group1"), 0, new HashMap<>())); } - // Tests for getQueryRequestBody / buildQueryResponse / getGetRequestBody / buildGetResponse - // were removed: get and query Utils helpers no longer exist (bulk-only module). - // ── deleteTokens error records must survive any JSON number type ────────── // recordMap holds deserialised JSON: Gson gives Double for numbers bound to Object, Jackson // gives Integer or Long by magnitude. A blind (Integer) cast turned a real API error into a diff --git a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java index 260e9053..9e49136e 100644 --- a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -11,12 +11,19 @@ import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.DeleteRequest; import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.QueryRequest; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.TokenizeRequest; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; import com.skyflow.vault.data.UpsertOptions; import org.junit.Assert; import org.junit.Test; @@ -1607,7 +1614,628 @@ public void testValidateBulkTokenizeRequest_nonSequentialIndexesAccepted() { } } - // Tests for validateQueryRequest / validateGetRequest were removed: - // those unary validators no longer exist (bulk-only module). + // ── validateQueryRequest ────────────────────────────────────────────────── + + @Test + public void testValidateQueryRequest_nullRequestThrows() { + try { + Validations.validateQueryRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.QueryRequestNull.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateQueryRequest_nullQueryThrows() { + QueryRequest request = QueryRequest.builder().build(); + try { + Validations.validateQueryRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.QueryKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateQueryRequest_blankQueryThrows() { + QueryRequest request = QueryRequest.builder().query(" ").build(); + try { + Validations.validateQueryRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyQuery.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateQueryRequest_validRequestDoesNotThrow() { + QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + try { + Validations.validateQueryRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateDeleteRequest ───────────────────────────────────────────────── + + @Test + public void testValidateDeleteRequest_nullRequestThrows() { + try { + Validations.validateDeleteRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.DeleteRequestNull.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_nullTableThrows() { + DeleteRequest request = DeleteRequest.builder().ids(Collections.singletonList("id1")).build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_blankTableThrows() { + DeleteRequest request = DeleteRequest.builder().table(" ").ids(Collections.singletonList("id1")).build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyTable.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_neitherIdsNorUniqueValuesThrows() { + DeleteRequest request = DeleteRequest.builder().table("table1").build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.IdsOrUniqueValuesKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_bothIdsAndUniqueValuesThrows() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + DeleteRequest request = DeleteRequest.builder() + .table("table1") + .ids(Collections.singletonList("id1")) + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.BothIdsAndUniqueValuesSpecified.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_blankIdInIdsThrows() { + DeleteRequest request = DeleteRequest.builder().table("table1").ids(Arrays.asList("id1", " ")).build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyIdInIds.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_emptyUniqueValueInUniqueValuesThrows() { + DeleteRequest request = DeleteRequest.builder() + .table("table1") + .uniqueValues(Collections.singletonList(new HashMap<>())) + .build(); + try { + Validations.validateDeleteRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyUniqueValueInUniqueValues.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateDeleteRequest_validWithIdsDoesNotThrow() { + DeleteRequest request = DeleteRequest.builder().table("table1").ids(Collections.singletonList("id1")).build(); + try { + Validations.validateDeleteRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateDeleteRequest_validWithUniqueValuesDoesNotThrow() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + DeleteRequest request = DeleteRequest.builder() + .table("table1") + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); + try { + Validations.validateDeleteRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateUpdateRequest ───────────────────────────────────────────────── + + @Test + public void testValidateUpdateRequest_nullRequestThrows() { + try { + Validations.validateUpdateRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.UpdateRequestNull.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_nullTableNameThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); + UpdateRequest request = UpdateRequest.builder().records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankTableNameThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); + UpdateRequest request = UpdateRequest.builder().tableName(" ").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyTable.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_nullRecordsThrows() { + UpdateRequest request = UpdateRequest.builder().tableName("table1").build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.RecordsKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_emptyRecordsThrows() { + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(new ArrayList<>()).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyRecords.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_nullRecordInListThrows() { + List records = new ArrayList<>(); + records.add(null); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.UpdateRecordNull.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_nullSkyflowIdThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.RecordSkyflowIdKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankSkyflowIdThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId(" ").build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptySkyflowIdInRecord.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankKeyInDataThrows() { + Map data = new HashMap<>(); + data.put(" ", "value1"); + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").data(data).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyKeyInRecords.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankValueInDataThrows() { + Map data = new HashMap<>(); + data.put("name", " "); + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").data(data).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyValueInValues.getMessage(), e.getMessage()); + } + } + @Test + public void testValidateUpdateRequest_emptyTokensMapThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").tokens(new HashMap<>()).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankKeyInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put(" ", "tok-abc"); + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").tokens(tokens).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyKeyInTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_blankValueInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put("name", " "); + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").tokens(tokens).build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_invalidUpdateTypeThrows() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); + UpdateRequest request = UpdateRequest.builder() + .tableName("table1") + .records(Collections.singletonList(record)) + .updateType("NOT_A_REAL_TYPE") + .build(); + try { + Validations.validateUpdateRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateUpdateRequest_validMinimalRequestDoesNotThrow() { + UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(Collections.singletonList(record)).build(); + try { + Validations.validateUpdateRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateUpdateRequest_validWithReplaceUpdateTypeAndRecordTableNameDoesNotThrow() { + Map data = new HashMap<>(); + data.put("name", "jane"); + UpdateRequestRecord record = UpdateRequestRecord.builder() + .skyflowId("sky1") + .data(data) + .tableName("table2") + .build(); + UpdateRequest request = UpdateRequest.builder() + .tableName("table1") + .records(Collections.singletonList(record)) + .updateType("REPLACE") + .build(); + try { + Validations.validateUpdateRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateGetRequest ──────────────────────────────────────────────────── + + @Test + public void testValidateGetRequest_nullRequestThrows() { + try { + Validations.validateGetRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.GetRequestNull.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_bothSingleTableFieldsAndRecordsThrows() { + GetRequestRecord nestedRecord = GetRequestRecord.builder().table("table2").ids(Collections.singletonList("id1")).build(); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .records(Collections.singletonList(nestedRecord)) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.BothSingleTableFieldsAndRecordsSpecified.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_nullRecordInRecordsListThrows() { + List records = new ArrayList<>(); + records.add(null); + GetRequest request = GetRequest.builder().records(records).build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.NullGetRecordRequest.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_nullTableThrows() { + GetRequest request = GetRequest.builder().ids(new ArrayList<>(Collections.singletonList("id1"))).build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_blankTableThrows() { + GetRequest request = GetRequest.builder().table(" ").ids(new ArrayList<>(Collections.singletonList("id1"))).build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyTable.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_neitherIdsNorUniqueValuesThrows() { + GetRequest request = GetRequest.builder().table("table1").build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.IdsOrUniqueValuesKeyError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_bothIdsAndUniqueValuesThrows() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.BothIdsAndUniqueValuesSpecified.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_blankIdInIdsThrows() { + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Arrays.asList("id1", " "))) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyIdInIds.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_emptyUniqueValueInUniqueValuesThrows() { + GetRequest request = GetRequest.builder() + .table("table1") + .uniqueValues(Collections.singletonList(new HashMap<>())) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyUniqueValueInUniqueValues.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_emptyFieldsThrows() { + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .fields(new ArrayList<>()) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyFields.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_blankFieldInFieldsThrows() { + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .fields(new ArrayList<>(Arrays.asList("name", " "))) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyFieldInFields.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_nullColumnRedactionInListThrows() { + List columnRedactions = new ArrayList<>(); + columnRedactions.add(null); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .columnRedactions(columnRedactions) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.NullColumnRedactions.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_blankColumnNameInColumnRedactionThrows() { + ColumnRedactions redaction = ColumnRedactions.builder().redaction("MASKED").build(); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .columnRedactions(Collections.singletonList(redaction)) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.NullColumnNameInColumnRedaction.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_blankRedactionInColumnRedactionThrows() { + ColumnRedactions redaction = ColumnRedactions.builder().columnName("email").build(); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .columnRedactions(Collections.singletonList(redaction)) + .build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.NullRedactionInColumnRedaction.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateGetRequest_validSingleTableWithIdsDoesNotThrow() { + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("id1"))) + .build(); + try { + Validations.validateGetRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateGetRequest_validSingleTableWithUniqueValuesDoesNotThrow() { + Map uniqueValue = new HashMap<>(); + uniqueValue.put("email", "john@example.com"); + GetRequest request = GetRequest.builder() + .table("table1") + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); + try { + Validations.validateGetRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateGetRequest_validMultiTableRecordsModeDoesNotThrow() { + GetRequestRecord nestedRecord1 = GetRequestRecord.builder() + .table("table1") + .ids(Collections.singletonList("id1")) + .build(); + GetRequestRecord nestedRecord2 = GetRequestRecord.builder() + .table("table2") + .ids(Collections.singletonList("id2")) + .build(); + GetRequest request = GetRequest.builder() + .records(Arrays.asList(nestedRecord1, nestedRecord2)) + .build(); + try { + Validations.validateGetRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateGetRequest_multiTableRecordMissingTableThrows() { + GetRequestRecord nestedRecord = GetRequestRecord.builder().ids(Collections.singletonList("id1")).build(); + GetRequest request = GetRequest.builder().records(Collections.singletonList(nestedRecord)).build(); + try { + Validations.validateGetRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableKeyError.getMessage(), e.getMessage()); + } + } } diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index aabfa2a9..bc67e377 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -12,14 +12,22 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; import com.skyflow.generated.rest.resources.flowservice.RawFlowserviceClient; +import com.skyflow.generated.rest.resources.records.RawRecordsClient; +import com.skyflow.generated.rest.resources.records.RecordsClient; +import com.skyflow.generated.rest.types.V1DeleteResponse; +import com.skyflow.generated.rest.types.V1DeleteResponseObject; import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1ExecuteQueryRecordResponse; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1GetResponse; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1UpdateResponse; import com.skyflow.utils.Constants; import com.skyflow.vault.data.BulkDeleteTokensOptions; import com.skyflow.vault.data.BulkTokenizeOptions; @@ -37,8 +45,24 @@ import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkDetokenizeOptions; import com.skyflow.vault.data.BulkInsertOptions; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.DeleteOptions; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; import com.skyflow.vault.data.DeleteTokensOptions; +import com.skyflow.vault.data.DetokenizeOptions; +import com.skyflow.vault.data.DetokenizeRequest; +import com.skyflow.vault.data.DetokenizeResponse; +import com.skyflow.vault.data.GetOptions; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.InsertOptions; +import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.QueryOptions; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; import com.skyflow.vault.data.RequestInterceptor; import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; @@ -46,6 +70,10 @@ import com.skyflow.vault.data.TokenizeRequestRecord; import com.skyflow.vault.data.TokenizeRequest; import com.skyflow.vault.data.TokenizeResponse; +import com.skyflow.vault.data.UpdateOptions; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; +import com.skyflow.vault.data.UpdateResponse; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; @@ -104,8 +132,270 @@ private static RawFlowserviceClient mockRawFlowservice(ApiClient mockApi) { return mockRaw; } - // Tests for the unary insert / detokenize / tokenize / deleteTokens controller methods - // (and their interceptor-header wiring) were removed: VaultController is bulk-only now. + private static RawRecordsClient mockRawRecords(ApiClient mockApi) { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + RawRecordsClient mockRaw = Mockito.mock(RawRecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.withRawResponse()).thenReturn(mockRaw); + return mockRaw; + } + + // ── insert (unary) ──────────────────────────────────────────────────────── + + @Test + public void testInsert_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord insertRecord = InsertRequestRecord.builder().data(data).build(); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(Collections.singletonList(insertRecord)) + .build(); + + InsertResponse response = controller.insert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-id-1", response.getRecords().get(0).getSkyflowId()); + Assert.assertNull(response.getRecords().get(0).getError()); + } + + @Test + public void testInsert_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + InsertRequest request = InsertRequest.builder().records(new ArrayList<>()).build(); + try { + controller.insert(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testInsert_interceptorAddsCustomHeader() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord insertRecord = InsertRequestRecord.builder().data(data).build(); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(Collections.singletonList(insertRecord)) + .build(); + + RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "acct-123"); + InsertOptions options = InsertOptions.builder().interceptor(interceptor).build(); + + controller.insert(request, options); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); + Mockito.verify(mockRaw).insert(any(), captor.capture()); + Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID.toString())); + } + + // ── detokenize (unary) ──────────────────────────────────────────────────── + + @Test + public void testDetokenize_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("tok-1").value("john@example.com").build(); + V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(Collections.singletonList("tok-1")).build(); + + DetokenizeResponse response = controller.detokenize(request); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("john@example.com", response.getRecords().get(0).getValue()); + } + + @Test + public void testDetokenize_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(new ArrayList<>()).build(); + try { + controller.detokenize(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── delete (unary) ──────────────────────────────────────────────────────── + + @Test + public void testDelete_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1DeleteResponseObject record = V1DeleteResponseObject.builder().skyflowId("sky-1").httpCode(200).build(); + V1DeleteResponse body = V1DeleteResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.delete(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + DeleteRequest request = DeleteRequest.builder().table("table1").ids(Collections.singletonList("sky-1")).build(); + + DeleteResponse response = controller.delete(request); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-1", response.getRecords().get(0).getSkyflowId()); + Assert.assertNull(response.getRecords().get(0).getError()); + } + + @Test + public void testDelete_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + DeleteRequest request = DeleteRequest.builder().table("table1").build(); + try { + controller.delete(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── update (unary) ──────────────────────────────────────────────────────── + + @Test + public void testUpdate_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-1").build(); + V1UpdateResponse body = V1UpdateResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.update(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "jane"); + UpdateRequestRecord updateRecord = UpdateRequestRecord.builder().skyflowId("sky-1").data(data).build(); + UpdateRequest request = UpdateRequest.builder() + .tableName("table1") + .records(Collections.singletonList(updateRecord)) + .build(); + + UpdateResponse response = controller.update(request); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-1", response.getRecords().get(0).getSkyflowId()); + } + + @Test + public void testUpdate_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + UpdateRequest request = UpdateRequest.builder().tableName("table1").records(new ArrayList<>()).build(); + try { + controller.update(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── get (unary) ─────────────────────────────────────────────────────────── + + @Test + public void testGet_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1RecordResponseObject record = V1RecordResponseObject.builder() + .tableName("table1").skyflowId("sky-1").build(); + V1GetResponse body = V1GetResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.get(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + GetRequest request = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Collections.singletonList("sky-1"))) + .build(); + + GetResponse response = controller.get(request); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-1", response.getRecords().get(0).getSkyflowId()); + } + + @Test + public void testGet_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + GetRequest request = GetRequest.builder().table("table1").build(); + try { + controller.get(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── query (unary) ───────────────────────────────────────────────────────── + + @Test + public void testQuery_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawRecordsClient mockRaw = mockRawRecords(mockApi); + + Map row = new HashMap<>(); + row.put("name", "john"); + V1ExecuteQueryRecordResponse record = V1ExecuteQueryRecordResponse.builder().data(row).build(); + V1ExecuteQueryResponse body = V1ExecuteQueryResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.flowServiceExecuteQuery(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + QueryRequest request = QueryRequest.builder().query("SELECT * FROM table1").build(); + + QueryResponse response = controller.query(request); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(row, response.getRecords().get(0).getData()); + } + + @Test + public void testQuery_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + QueryRequest request = QueryRequest.builder().build(); + try { + controller.query(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } // ── bulkInsert ──────────────────────────────────────────────────────────── @@ -776,8 +1066,6 @@ public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { Assert.assertEquals("group1", field1Tokens.get(0).getTokenGroupName()); } - // Tests for the unary query / get controller methods were removed: VaultController is bulk-only now. - // ───────────────────────────────────────────────────────────────────────── // Request fidelity through batch dispatch //