diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index f7f65aa3..1e408cca 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -90,21 +90,25 @@ public enum ErrorMessage { InsufficientTokensPassedForTokenModeEnableStrict("%s0 Validation error. 'tokenMode' is set to 'ENABLE_STRICT', but some fields are missing tokens. Specify tokens for all fields."), BatchInsertPartialSuccess("%s0 Insert operation completed with partial success."), BatchInsertFailure("%s0 Insert operation failed."), - RecordSizeExceedError("%s0 Maximum number of records exceeded. The limit is 10000."), + RecordSizeExceedError("%s0 Maximum number of records exceeded. The limit is 100000."), // Detokenize InvalidDetokenizeData("%s0 Validation error. Invalid detokenize data. Specify valid detokenize data."), EmptyDetokenizeData("%s0 Validation error. Invalid data tokens. Specify at least one data token."), EmptyTokenInDetokenizeData("%s0 Validation error. Invalid data tokens. Specify a valid data token."), - TokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), + TokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 100000."), + + // 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."), EmptyTokenInDeleteTokensData("%s0 Validation error. Invalid token in delete tokens request. Specify a valid token."), - DeleteTokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), + DeleteTokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 100000."), // 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"), @@ -156,7 +165,7 @@ public enum ErrorMessage { EmptyValueInTokenizeRecord("%s0 Validation error. Value in TokenizeRecord is null or empty. Specify a valid value."), EmptyTokenGroupNamesInTokenizeRecord("%s0 Validation error. TokenGroupNames in TokenizeRecord is null or empty. Specify at least one token group name."), EmptyTokenGroupNameInTokenizeRecord("%s0 Validation error. Token group name in TokenizeRecord is null or empty. Specify a valid token group name."), - TokenizeDataSizeExceedError("%s0 Maximum number of tokenize records exceeded. The limit is 10000."), + TokenizeDataSizeExceedError("%s0 Maximum number of tokenize records exceeded. The limit is 100000."), MissingIndexInBulkTokenizeRecord("%s0 Validation error. Index in BulkTokenizeRequestRecord is null. Specify an index for every record."), DuplicateIndexInBulkTokenizeRecord("%s0 Validation error. Duplicate index in BulkTokenizeRequestRecord. Specify a unique index for every record."), diff --git a/common/src/main/java/com/skyflow/logs/ErrorLogs.java b/common/src/main/java/com/skyflow/logs/ErrorLogs.java index 8f1955e8..418d08bc 100644 --- a/common/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/common/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -62,8 +62,8 @@ public enum ErrorLogs { RECORDS_IS_REQUIRED("Invalid %s1 request. Records are required."), EMPTY_RECORDS("Invalid %s1 request. Records can not be empty."), INVALID_RECORD("Invalid %s1 request. Invalid record. Specify a valid record."), - RECORD_SIZE_EXCEED("Maximum number of records exceeded. The limit is 10000."), - TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 10000."), + RECORD_SIZE_EXCEED("Maximum number of records exceeded. The limit is 100000."), + TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 100000."), EMPTY_OR_NULL_VALUE_IN_VALUES("Invalid %s1 request. Value can not be null or empty in values for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_VALUES("Invalid %s1 request. Key can not be null or empty in values"), NULL_VALUE_ENTRY_IN_VALUES("Invalid %s1 request. Value entry in 'values' can not be null at index %s2."), @@ -144,7 +144,7 @@ public enum ErrorLogs { EMPTY_VALUE_IN_TOKENIZE_RECORD("Invalid %s1 request. Value in TokenizeRecord can not be null or empty."), EMPTY_TOKEN_GROUP_NAMES_IN_TOKENIZE_RECORD("Invalid %s1 request. TokenGroupNames in TokenizeRecord can not be null or empty."), EMPTY_TOKEN_GROUP_NAME_IN_TOKENIZE_RECORD("Invalid %s1 request. Token group name in TokenizeRecord can not be null or empty at index %s2."), - TOKENIZE_DATA_SIZE_EXCEED("Maximum number of tokenize records exceeded. The limit is 10000."), + TOKENIZE_DATA_SIZE_EXCEED("Maximum number of tokenize records exceeded. The limit is 100000."), MISSING_INDEX_IN_BULK_TOKENIZE_RECORD("Invalid %s1 request. Index in BulkTokenizeRequestRecord can not be null at position %s2."), DUPLICATE_INDEX_IN_BULK_TOKENIZE_RECORD("Invalid %s1 request. Duplicate index %s2 in BulkTokenizeRequestRecord."), DELETE_REQUEST_NULL("Invalid %s1 request. Delete request can not be null."), @@ -152,7 +152,7 @@ public enum ErrorLogs { DELETE_TOKENS_REQUEST_NULL("Invalid %s1 request. DeleteTokens request can not be null."), EMPTY_DELETE_TOKENS_DATA("Invalid %s1 request. Delete tokens data can not be empty."), EMPTY_OR_NULL_TOKEN_IN_DELETE_TOKENS_DATA("Invalid %s1 request. Token can not be null or empty in delete tokens data at index %s2."), - DELETE_TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 10000."), + DELETE_TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 100000."), DELETE_TOKENS_REQUEST_REJECTED("DeleteTokens request resulted in failure."), // invoke connection interface diff --git a/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java b/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java index 1fb01e6e..eb7e0706 100644 --- a/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java +++ b/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java @@ -192,6 +192,22 @@ public void testNonJsonBodyFallsBackToRawBodyAsMessage() { Assert.assertEquals("plain text error response", ex.getMessage()); } + @Test + public void testJsonBodyWithGenericApiErrorEnvelopeStillWorks() { + Map> headers = new HashMap<>(); + String json = "{\"error\":{\"grpc_code\":5,\"http_code\":404," + + "\"message\":\"Invalid request. Vault not found for vaultID: vault123. " + + "Specify a valid vaultID.\",\"http_status\":\"Not Found\",\"details\":[]}}"; + SkyflowException ex = new SkyflowException(404, new RuntimeException("fail"), headers, json); + Assert.assertEquals("Invalid request. Vault not found for vaultID: vault123. " + + "Specify a valid vaultID.", ex.getMessage()); + Assert.assertEquals(Integer.valueOf(5), ex.getGrpcCode()); + Assert.assertEquals("Not Found", ex.getHttpStatus()); + Assert.assertEquals(404, ex.getHttpCode()); + Assert.assertNotNull(ex.getDetails()); + Assert.assertEquals(0, ex.getDetails().size()); + } + @Test public void testNullBodyNullCauseMessageFallsBackToErrorOccurred() { Map> headers = new HashMap<>(); diff --git a/flowvault/README.md b/flowvault/README.md index 13778be3..4be55794 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -30,10 +30,19 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope - [VaultController — Bulk operations](#vaultcontroller--bulk-operations) - [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults) - [Batching and concurrency](#batching-and-concurrency) +- [VaultController — Unary operations](#vaultcontroller--unary-operations) + - [Unary vs. bulk](#unary-vs-bulk) + - [Vault type support](#vault-type-support) - [Bulk Insert](#bulk-insert) - [Bulk Tokenize](#bulk-tokenize) - [Bulk Detokenize](#bulk-detokenize) - [Bulk Delete Tokens](#bulk-delete-tokens) +- [Insert](#insert) +- [Detokenize](#detokenize) +- [Get](#get) +- [Update](#update) +- [Delete](#delete) +- [Query](#query) - [Custom Request Headers](#custom-request-headers) - [Error Handling](#error-handling) - [Two layers of errors](#two-layers-of-errors) @@ -46,6 +55,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope - Authenticate using a Skyflow service account, an API key, or a bearer token — see [Authenticate](#authenticate). - Perform bulk Vault API operations — insert, tokenize, detokenize, and delete tokens — each with a synchronous and an async variant, built for high-throughput Flow DB workloads. +- Perform unary Vault API operations — insert, detokenize, get, update, delete, and query — a single API call each, for when you want a plain request and response rather than the bulk batching machinery. See [VaultController — Unary operations](#vaultcontroller--unary-operations). - **Per-record reporting, not all-or-nothing.** A bulk call succeeds as a call even when individual records fail; every response reports a summary plus the outcome of each individual record or token. See [Error Handling](#error-handling). # Install @@ -354,6 +364,50 @@ INSERT_CONCURRENCY_LIMIT=5 The 10,000-item ceiling per bulk call is a separate, fixed limit and is not configurable. +# VaultController — Unary operations + +Alongside the bulk methods, `VaultController` exposes six **unary** operations. Each sends exactly one API call and hands the result straight back: + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `insert(InsertRequest)` | `InsertRequest`, optional `InsertOptions` | `InsertResponse` | Insert records, optionally across multiple tables, in one call | +| `detokenize(DetokenizeRequest)` | `DetokenizeRequest`, optional `DetokenizeOptions` | `DetokenizeResponse` | Detokenize tokens, optionally with a redaction override per token group | +| `get(GetRequest)` | `GetRequest`, optional `GetOptions` | `GetResponse` | Read records by skyflow ID or unique value, optionally with a redaction override per column | +| `update(UpdateRequest)` | `UpdateRequest`, optional `UpdateOptions` | `UpdateResponse` | Update records by skyflow ID | +| `delete(DeleteRequest)` | `DeleteRequest`, optional `DeleteOptions` | `DeleteResponse` | Delete records by skyflow ID or unique value | +| `query(QueryRequest)` | `QueryRequest`, optional `QueryOptions` | `QueryResponse` | Run a SQL-style query against the vault | + +`insert` and `detokenize` are the unary counterparts of `bulkInsert` and `bulkDetokenize` — the same request builders, sent as one call instead of many batches. `get`, `update`, `delete`, and `query` have no bulk counterpart at all; they exist only in this unary form. + +Each method also accepts an optional options object (`InsertOptions`, `DetokenizeOptions`, `GetOptions`, `UpdateOptions`, `DeleteOptions`, `QueryOptions`) — see [Custom Request Headers](#custom-request-headers). + +## Unary vs. bulk + +Everything the bulk machinery adds — batching, concurrency, the payload ceiling, the summary, the per-item index — is absent here. What survives is the per-record reporting: + +| | Bulk operations | Unary operations | +|---|---|---| +| Async variant | Yes — `bulkInsertAsync`, and so on | **No.** Wrap the call yourself if you need one | +| Batching and concurrency | Configured per operation — see [Batching and concurrency](#batching-and-concurrency) | Not applicable — one payload, one call | +| Payload ceiling | 10,000 records or tokens per call | Not enforced by the SDK; the vault's own request limits still apply | +| Response summary | `getSummary()` | None — read the records list | +| Per-item `getIndex()` / `getRequestId()` | Yes | No. Records come back in submitted order, and the `x-request-id` of the single call reaches you only through a thrown `SkyflowException` | +| Retry helper | `getRecordsToRetry()` / `getTokensToRetry()` | None — filter the records yourself, see [Retrying the failed records](#retrying-the-failed-records) | +| Per-item `getHttpCode()` / `getError()` | Yes | Yes, on every unary operation except `query` | + +## Vault type support + +The same distinction as [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults) applies. Five of the six unary operations address records inside a table, so they only make sense against a structured vault: + +| Operation | Supported on | +|---|---| +| `insert` | Structured (schema) vaults — inserts into a table's columns. | +| `get` | Structured vaults — reads a table's records by skyflow ID or unique value. | +| `update` | Structured vaults — updates a table's records by skyflow ID. | +| `delete` | Structured vaults — deletes a table's records. Distinct from `bulkDeleteTokens`, which removes tokens only and leaves the record in place. | +| `query` | Structured vaults — the query itself addresses tables and columns. | +| `detokenize` | Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. | + # Bulk Insert Insert many records — even across different tables — in a single call. Each record is a `BulkInsertRequestRecord` with its own `data` and, optionally, its own `tableName` and `upsert`. @@ -659,7 +713,7 @@ Sample response: } ``` -`record.getMetadata()` is typed as a `DetokenizeMetadata` with `getSkyflowId()`/`getTableName()` — no casting into the raw map required (`null` on records that errored, same as above): +`record.getMetadata()` is typed as a `DetokenizeResponseRecordMetadata` with `getSkyflowId()`/`getTableName()` — no casting into the raw map required (`null` on records that errored, same as above): ```java for (BulkDetokenizeResponseRecord record : detokenizeResponse.getRecords()) { @@ -735,9 +789,541 @@ for (BulkDeleteTokensResponseRecord record : deleteTokensResponse.getRecords()) Use `deleteTokensResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. +# Insert + +Insert records in a single API call — the unary counterpart of [Bulk Insert](#bulk-insert), with no batching or concurrency involved. Each record is an `InsertRequestRecord` with its own `data` and, optionally, its own `tableName`, `tokens`, and `upsert`. + +> **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support). + +**Note:** + +- `tableName` must be specified at exactly one level: either on the request (`InsertRequest.builder().tableName(...)`) or on **every** record (`InsertRequestRecord.builder().tableName(...)`) — not both, and not neither. Same rule as bulk insert. +- `upsert` is optional, but wherever you supply it, it must sit at the same level as `tableName`. +- `tokens` is optional; when supplied, the map must not be empty and no key or value may be blank. + +### Construct an insert request + +```java +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class InsertExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Build each record. Here tableName lives on the request, so no record carries it. + Map recordData = new HashMap<>(); + recordData.put("card_number", "4111111111111111"); + recordData.put("cardholder_name", "john doe"); + + InsertRequestRecord record = InsertRequestRecord.builder() + .data(recordData) + .build(); + + List records = new ArrayList<>(); + records.add(record); + + // Step 2: Build the InsertRequest + InsertRequest insertRequest = InsertRequest.builder() + .tableName("table1") + .records(records) + .build(); + + // Step 3: Perform the insert + InsertResponse insertResponse = vault.insert(insertRequest); + System.out.println(insertResponse); + } +} +``` + +To put the table name on the records instead, drop `tableName` from the request and set it — along with any `upsert` — on every `InsertRequestRecord`, exactly as [Bulk Insert](#bulk-insert) shows. + +There is no async variant: `insert` returns its `InsertResponse` directly. + +Sample response: + +```json +{ + "records": [ + { + "tableName": "table1", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "tokens": { + "card_number": [ + { "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" } + ], + "cardholder_name": [ + { "token": "b2308e2a-c1f5-469b-97b7-1f193159399b", "tokenGroupName": "deterministic_string" } + ] + }, + "data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" }, + "hashedData": { "card_number": "b6e6d...c3f9" }, + "httpCode": 200, + "error": null, + "requestId": null + } + ] +} +``` + +There is no `summary` and no per-record `index` — the records come back in the order you submitted them. `requestId` behaves exactly as it does on a bulk record: `null` on success, the failing call's `x-request-id` on error. `getTokens()` returns the same parsed `Map>` described under [Bulk Insert](#bulk-insert), so `Token.getToken()`, `Token.getTokenGroupName()`, and `Token.getPath()` are available with no casting. + +Accessors: `insertResponse.getRecords()`, and on each record `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +> **Deprecation notice:** `InsertResponseRecord.getFields()` is deprecated in favor of `getTokens()` here too — it is kept only for backward compatibility and will be removed in a future release. + +```java +for (InsertResponseRecord record : insertResponse.getRecords()) { + if (record.getError() == null) { + System.out.println(record.getTableName() + " -> " + record.getSkyflowId()); + } else { + System.err.println("insert failed [" + record.getHttpCode() + "] " + record.getError()); + } +} +``` + +# Detokenize + +Detokenize tokens in a single API call — the unary counterpart of [Bulk Detokenize](#bulk-detokenize), optionally overriding the redaction applied per token group via `tokenGroupRedactions`. + +> **Vault type supported:** both. See [Vault type support](#vault-type-support). + +**Note:** + +- `tokens` is required and must not be empty, and no entry may be blank. +- `tokenGroupRedactions` is optional; when supplied, each entry needs a non-blank `tokenGroupName` and `redaction`. + +### Construct a detokenize request + +```java +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.Arrays; +import java.util.List; + +public class DetokenizeExample { + public static void main(String[] args) throws SkyflowException { + List tokens = new ArrayList<>(Arrays.asList( + "5479-4229-4622-1393", + "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + )); + + // redaction is a free-form string understood by the vault (e.g. "PLAIN_TEXT", + // "MASKED", "REDACTED", "DEFAULT") + TokenGroupRedactions redaction = TokenGroupRedactions.builder() + .tokenGroupName("card_number_cg") + .redaction("MASKED") + .build(); + + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Arrays.asList(redaction)) + .build(); + + DetokenizeResponse detokenizeResponse = vault.detokenize(detokenizeRequest); + System.out.println(detokenizeResponse); + } +} +``` + +There is no async variant: `detokenize` returns its `DetokenizeResponse` directly. + +Sample response: + +```json +{ + "records": [ + { + "token": "5479-4229-4622-1393", + "value": "4111111111111111", + "tokenGroupName": "card_number_cg", + "metadata": { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "tableName": "table1" }, + "httpCode": 200, + "error": null, + "requestId": null + }, + { + "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "value": null, + "tokenGroupName": null, + "metadata": null, + "httpCode": 404, + "error": "Token Not Found", + "requestId": "a1b2c3d4-..." + } + ] +} +``` + +Same per-record shape as bulk detokenize, minus `index` — `requestId` behaves the same on both: `null` on success, the failing call's `x-request-id` on error. `record.getMetadata()` is a typed `DetokenizeResponseRecordMetadata` with `getSkyflowId()`/`getTableName()` — `null` on records that errored: + +```java +for (DetokenizeResponseRecord record : detokenizeResponse.getRecords()) { + if (record.getError() == null) { + System.out.println(record.getToken() + " -> " + record.getValue() + + " (" + record.getTokenGroupName() + ")"); + } else { + System.err.println(record.getToken() + " failed [" + + record.getHttpCode() + "] " + record.getError()); + } +} +``` + +Accessors: `detokenizeResponse.getRecords()`, and on each record `getToken()`, `getValue()`, `getTokenGroupName()`, `getMetadata()`, `getHttpCode()`, `getError()`, `getRequestId()`. `getValue()` is typed `Object`, passed straight through from the API. + +# Get + +Read records back from a table, by skyflow ID or by unique value, optionally overriding the redaction applied per column via `columnRedactions`. + +> **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support). + +**Note:** + +- A `GetRequest` works in one of two modes, and they are mutually exclusive: **single-table** (`table`, `ids`/`uniqueValues`, `fields`, `columnRedactions`, `limit`, `offset`) or **multi-table** (`records`, a list of `GetRequestRecord`). Setting fields from both modes fails validation. +- `table` is required, and exactly one of `ids` or `uniqueValues` must be supplied — both, or neither, fails validation. This holds per record in multi-table mode. +- `uniqueValues` is a `List>`: one map per record, each holding the unique column-name/value pairs that identify it. +- `fields` selects the columns to return; omit it for all of them. When supplied, it must be non-empty with no blank entries. +- `limit` and `offset` apply to the call as a whole and are **only sent in single-table mode** — a `GetRequestRecord` has no `limit`/`offset` of its own, and values set on a multi-table request are not sent. + +### Construct a get request + +```java +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class GetExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Optionally override how individual columns come back. Anything not listed + // uses the redaction configured on the vault's policy. + ColumnRedactions redaction = ColumnRedactions.builder() + .columnName("card_number") + .redaction("MASKED") + .build(); + + // Step 2: Build the GetRequest — single-table mode, selecting records by skyflow ID + GetRequest getRequest = GetRequest.builder() + .table("table1") + .ids(new ArrayList<>(Arrays.asList( + "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "b2308e2a-c1f5-469b-97b7-1f193159399b"))) + .fields(new ArrayList<>(Arrays.asList("card_number", "cardholder_name"))) + .columnRedactions(Collections.singletonList(redaction)) + .limit(10) + .offset(0) + .build(); + + // Step 3: Perform the get + GetResponse getResponse = vault.get(getRequest); + System.out.println(getResponse); + } +} +``` + +To select records by unique value instead of skyflow ID, swap `ids(...)` for `uniqueValues(...)`: + +```java +Map uniqueValue = new HashMap<>(); +uniqueValue.put("email", "jane.doe@example.com"); + +GetRequest getRequest = GetRequest.builder() + .table("table2") + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); +``` + +To read from more than one table in a single call, use multi-table mode — each `GetRequestRecord` carries its own table and lookup fields, and none of the single-table fields may be set on the request itself: + +```java +GetRequestRecord fromTable1 = GetRequestRecord.builder() + .table("table1") + .ids(Arrays.asList("9fac9201-7b8a-4446-93f8-5244e1213bd1")) + .fields(Arrays.asList("card_number")) + .build(); + +GetRequestRecord fromTable2 = GetRequestRecord.builder() + .table("table2") + .uniqueValues(Collections.singletonList(uniqueValue)) + .build(); + +GetRequest getRequest = GetRequest.builder() + .records(Arrays.asList(fromTable1, fromTable2)) + .build(); +``` + +There is no async variant: `get` returns its `GetResponse` directly. + +Sample response: + +```json +{ + "records": [ + { + "tableName": "table1", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "tokens": { + "card_number": [ + { "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" } + ] + }, + "data": { "card_number": "4111-XXXX-XXXX-1111", "cardholder_name": "John Doe" }, + "hashedData": null, + "httpCode": 200, + "error": null, + "requestId": null + } + ] +} +``` + +`GetResponseRecord` carries the same fields as an insert record — the vault returns the same object shape for both — so the accessors are identical: `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +```java +for (GetResponseRecord record : getResponse.getRecords()) { + if (record.getError() == null) { + System.out.println(record.getSkyflowId() + " -> " + record.getData()); + } else { + System.err.println(record.getSkyflowId() + " failed [" + + record.getHttpCode() + "] " + record.getError()); + } +} +``` + +# Update + +Update records in a table by skyflow ID, in a single API call. + +> **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support). + +**Note:** + +- `tableName` is required on the request. A record may override it with its own `tableName`, which applies to that record only. +- Every `UpdateRequestRecord` needs a non-blank `skyflowId`. +- `data` holds the columns to change; no key or value may be blank. `tokens` is optional, and when supplied must be non-empty with no blank keys or values. +- `updateType` accepts `"UPDATE"` (merge the supplied columns) or `"REPLACE"` (overwrite the whole record). Any other value fails validation; omitting it sends no `updateType`, which the vault treats the same as `"UPDATE"`. It is a request-level setting — records have no `updateType` of their own. + +### Construct an update request + +```java +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.Collections; +import java.util.HashMap; +import java.util.Map; + +public class UpdateExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Build the record — the columns to change, keyed by the record's skyflow ID + Map data = new HashMap<>(); + data.put("cardholder_name", "jane doe"); + + UpdateRequestRecord record = UpdateRequestRecord.builder() + .skyflowId("9fac9201-7b8a-4446-93f8-5244e1213bd1") + .data(data) + .build(); + + // Step 2: Build the UpdateRequest + UpdateRequest updateRequest = UpdateRequest.builder() + .tableName("table1") + .records(Collections.singletonList(record)) + .updateType("UPDATE") + .build(); + + // Step 3: Perform the update + UpdateResponse updateResponse = vault.update(updateRequest); + System.out.println(updateResponse); + } +} +``` + +There is no async variant: `update` returns its `UpdateResponse` directly. + +Sample response: + +```json +{ + "records": [ + { + "tableName": "table1", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "tokens": { + "cardholder_name": [ + { "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "tokenGroupName": "deterministic_string" } + ] + }, + "data": { "cardholder_name": "Jane Doe" }, + "hashedData": null, + "httpCode": 200, + "error": null, + "requestId": null + } + ] +} +``` + +Like `GetResponseRecord`, `UpdateResponseRecord` carries the insert record's fields — the vault returns the same object shape — so the accessors are `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +```java +for (UpdateResponseRecord record : updateResponse.getRecords()) { + if (record.getError() == null) { + System.out.println(record.getSkyflowId() + " updated"); + } else { + System.err.println(record.getSkyflowId() + " failed [" + + record.getHttpCode() + "] " + record.getError()); + } +} +``` + +# Delete + +Delete records from a table by skyflow ID or unique value, in a single API call. + +> **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support). + +**Note:** + +- This deletes the records themselves. [Bulk Delete Tokens](#bulk-delete-tokens) is a different operation — it removes tokens and leaves the underlying record in place. +- `table` is required, and exactly one of `ids` or `uniqueValues` must be supplied — both, or neither, fails validation. +- `uniqueValues` takes the same shape as in [Get](#get): one `Map` per record, holding the unique column-name/value pairs that identify it. + +### Construct a delete request + +```java +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.Arrays; +import java.util.List; + +public class DeleteExample { + public static void main(String[] args) throws SkyflowException { + List ids = new ArrayList<>(Arrays.asList( + "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "b2308e2a-c1f5-469b-97b7-1f193159399b" + )); + + DeleteRequest deleteRequest = DeleteRequest.builder() + .table("table1") + .ids(ids) + .build(); + + DeleteResponse deleteResponse = vault.delete(deleteRequest); + System.out.println(deleteResponse); + } +} +``` + +There is no async variant: `delete` returns its `DeleteResponse` directly. + +Sample response: + +```json +{ + "records": [ + { "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", "httpCode": 200, "error": null, "requestId": null }, + { "skyflowId": "b2308e2a-c1f5-469b-97b7-1f193159399b", "httpCode": 404, "error": "Record Not Found", "requestId": "a1b2c3d4-..." } + ] +} +``` + +`DeleteResponseRecord` is flatter than the insert-shaped records above — the vault returns no data, tokens, or hashed data for a delete. Accessors: `deleteResponse.getRecords()`, and on each record `getSkyflowId()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +```java +for (DeleteResponseRecord record : deleteResponse.getRecords()) { + if (record.getError() == null) { + System.out.println(record.getSkyflowId() + " deleted"); + } else { + System.err.println(record.getSkyflowId() + " failed [" + + record.getHttpCode() + "] " + record.getError()); + } +} +``` + +# Query + +Run a SQL-style query against the vault in a single API call. + +> **Vault type supported:** structured (schema) vaults. See [Vault type support](#vault-type-support). + +**Note:** + +- `query` is required and must not be blank. That is the whole request — there are no other fields. +- This is the one unary operation with **no per-record status**: rows either come back or the call throws. There is no `httpCode` or `error` on a `QueryResponseRecord`. + +### Construct a query request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.QueryResponseRecord; + +public class QueryExample { + public static void main(String[] args) throws SkyflowException { + QueryRequest queryRequest = QueryRequest.builder() + .query("SELECT card_number, cardholder_name FROM table1 LIMIT 10") + .build(); + + QueryResponse queryResponse = vault.query(queryRequest); + System.out.println(queryResponse); + } +} +``` + +There is no async variant: `query` returns its `QueryResponse` directly. + +Sample response: + +```json +{ + "records": [ + { "data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" } }, + { "data": { "card_number": "5484-7829-1702-9110", "cardholder_name": "Jane Doe" } } + ], + "metadata": { + "columns": ["card_number", "cardholder_name"] + } +} +``` + +Each row is a free-form column/value map — the query API has no notion of tokens, so unlike insert or detokenize there is no typed `Token` data here. `getMetadata()` returns a `QueryResponseMetadata` wrapping the query's return columns via `getColumns()`; both `getMetadata()` and `getColumns()` are `null` when the vault doesn't report columns. + +Accessors: `queryResponse.getRecords()` and `queryResponse.getMetadata()`, and on each record `getData()`. + +```java +System.out.println("columns: " + (queryResponse.getMetadata() != null ? queryResponse.getMetadata().getColumns() : null)); +for (QueryResponseRecord row : queryResponse.getRecords()) { + System.out.println(row.getData()); +} +``` + # Custom Request Headers -To include custom HTTP headers on an outgoing bulk request, pass a `RequestInterceptor` via that operation's options object. The headers available are defined by the `CustomHeaderKey` enum: +To include custom HTTP headers on an outgoing request — bulk or unary — pass a `RequestInterceptor` via that operation's options object. The headers available are defined by the `CustomHeaderKey` enum: | `CustomHeaderKey` | HTTP header name | |---|---| @@ -756,9 +1342,9 @@ BulkInsertOptions options = BulkInsertOptions.builder() BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest, options); ``` -The interceptor runs **once per batch**, not once per bulk call — so a value generated inside it (a fresh request id, say) differs between the batches a single bulk call is split into. +The interceptor runs **once per batch**, not once per bulk call — so a value generated inside it (a fresh request id, say) differs between the batches a single bulk call is split into. On a unary operation there is only ever one call, so it runs exactly once. -The same pattern applies to every bulk operation, via its corresponding options class: +The same pattern applies to every operation, via its corresponding options class: | Operation | Options class | |---|---| @@ -766,19 +1352,27 @@ The same pattern applies to every bulk operation, via its corresponding options | `bulkTokenize` / `bulkTokenizeAsync` | `BulkTokenizeOptions` | | `bulkDetokenize` / `bulkDetokenizeAsync` | `BulkDetokenizeOptions` | | `bulkDeleteTokens` / `bulkDeleteTokensAsync` | `BulkDeleteTokensOptions` | +| `insert` | `InsertOptions` | +| `detokenize` | `DetokenizeOptions` | +| `get` | `GetOptions` | +| `update` | `UpdateOptions` | +| `delete` | `DeleteOptions` | +| `query` | `QueryOptions` | # Error Handling ## Two layers of errors -This is the mental model to hold for every bulk operation: +This is the mental model to hold for every operation, bulk or unary: | Layer | What it covers | How you see it | |---|---|---| | **Request-level** | The call could not be made or the whole call failed: invalid request shape, missing credentials, auth failure, payload over the 10,000-item limit. | A thrown `SkyflowException`. No results at all. | | **Record-level** | The call succeeded, but individual records or tokens inside it did not. | A returned response. **Nothing is thrown.** Each entry in `getRecords()` reports its own `httpCode` and `error`. | -The second layer is what distinguishes `flowvault` from an all-or-nothing API: **a bulk call that returns normally can still contain failures, and a call where every single record failed also returns normally rather than throwing.** Checking only for a thrown exception will silently miss failed records — always read the summary and the per-record results. +The second layer is what distinguishes `flowvault` from an all-or-nothing API: **a call that returns normally can still contain failures, and a call where every single record failed also returns normally rather than throwing.** Checking only for a thrown exception will silently miss failed records — always read the summary and the per-record results. + +Unary operations follow the same two layers. Their records carry the same `requestId` behavior as bulk records — `null` on success, the failing call's `x-request-id` on error — the only structural differences are that unary records have no `getIndex()` (there is no batch position to report), and `query` has no record-level layer at all — rows either come back or the call throws, so `QueryResponse` carries no per-record `requestId` either. ## Per-record success and failure @@ -786,10 +1380,10 @@ Every bulk response exposes `getSummary()` and `getRecords()`. The records list | Field | Present on | Meaning | |---|---|---| -| `getIndex()` | always | Position of this item in the payload you submitted — use it to line results back up with your input. | +| `getIndex()` | bulk only | Position of this item in the payload you submitted — use it to line results back up with your input. | | `getHttpCode()` | always | Per-item status. `2xx` for success; `4xx`/`5xx` for failure. | | `getError()` | failures only | Error message for this item. `null` means this item succeeded. | -| `getRequestId()` | failures only | The `x-request-id` of the batch this item was in — quote it in support escalations. Items from the same batch share one id. | +| `getRequestId()` | failures only | The `x-request-id` of the call this item was part of — quote it in support escalations. In bulk responses, items from the same batch share one id. Present on both bulk and unary per-record types; `QueryResponse` is the one exception (see below). | The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getTokens()`/`getData()` for insert (`getFields()` is deprecated — it returns the same data in its original, pre-typed `Map` shape, not `getTokens()`'s `Token` objects), `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getValue()`/`getTokenGroupName()`/`getToken()` for tokenize, `getToken()` for delete. @@ -802,6 +1396,8 @@ Summaries per operation: | `BulkDetokenizeResponse` | `DetokenizeSummary` | `totalTokens`, `totalDetokenized`, `totalFailed` | | `BulkDeleteTokensResponse` | `DeleteTokensSummary` | `totalTokens`, `totalDeleted`, `totalFailed` | +A unary response has no summary and no `getIndex()` — just `getRecords()`, in submitted order, with `getHttpCode()`, `getError()`, and `getRequestId()` on each entry alongside that operation's payload: `getSkyflowId()`/`getTokens()`/`getData()`/`getHashedData()` for `insert`, `get`, and `update`; `getSkyflowId()` alone for `delete`; `getToken()`/`getValue()`/`getTokenGroupName()`/`getMetadata()` for `detokenize`. `QueryResponse` is the exception — its records carry only `getData()`, with no `getRequestId()` anywhere on the response, and the call's return columns on `getMetadata().getColumns()`. + The idiomatic way to consume a bulk response: ```java diff --git a/flowvault/api-report/skyflow-flowvault-java.baseline.jar b/flowvault/api-report/skyflow-flowvault-java.baseline.jar index 1294715f..591acef4 100644 Binary files a/flowvault/api-report/skyflow-flowvault-java.baseline.jar and b/flowvault/api-report/skyflow-flowvault-java.baseline.jar differ 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..347b9557 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/DeleteExample.java @@ -0,0 +1,78 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.*; + +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. + // Running this actually removes the record — rerunning GetExample/UpdateExample + // against the same skyflowId afterward will then fail, since it's gone. + List ids = new ArrayList<>(); + ids.add(""); + + // Step 5: Build and execute the delete request + DeleteRequest request = DeleteRequest.builder() + .table("") + .ids(ids) + .build(); + + DeleteOptions options = DeleteOptions.builder() + .interceptor(ctx -> { + ctx.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "DeleteOptions"); // pass the request id here + }) + .build(); + DeleteResponse response = skyflowClient.vault().delete(request, options); + + // 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); + } + } +} 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..4f077223 --- /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..b9c539b2 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/GetExample.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.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.setVaultUrl(""); +// 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(""); + 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()); + System.out.println("dta"+ record.getData()); + +// 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); + } + } +} 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..0725b409 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/InsertExample.java @@ -0,0 +1,84 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.*; + +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) + .tableName("") + .build(); + + List records = new ArrayList<>(); + records.add(record); + + // Step 5: Build and execute the insert request + InsertRequest request = InsertRequest.builder() +// .tableName("") + .records(records) + .build(); + InsertOptions options = InsertOptions.builder() + .interceptor(ctx -> { + ctx.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "demo"); // pass the request id here + }) + .build(); + InsertResponse response = skyflowClient.vault().insert(request, options); + + // Step 6: Print every field on each response record. + for (InsertResponseRecord insertedRecord : response.getRecords()) { + System.out.println("tableName:\t" + insertedRecord.getTableName()); + System.out.println("skyflowId:\t" + insertedRecord.getSkyflowId()); + System.out.println("tokens:\t\t" + insertedRecord.getTokens()); + System.out.println("data:\t\t" + insertedRecord.getData()); + System.out.println("hashedData:\t" + insertedRecord.getHashedData()); + System.out.println("httpCode:\t" + insertedRecord.getHttpCode()); + System.out.println("error:\t\t" + insertedRecord.getError()); +// System.out.println("request id " +insertedRecord.ge); + } + } catch (SkyflowException e) { + // Step 7: Handle any errors that occur during the process + System.err.println("Error in insert operation:\t" + e); + } + } +} 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..4dd2e61e --- /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.getMetadata() != null ? response.getMetadata().getColumns() : null)); + } 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..9b91b348 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/UpdateExample.java @@ -0,0 +1,82 @@ +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.enums.UpdateType; +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 UpdateType.UPDATE (default) or UpdateType.REPLACE. + UpdateRequest request = UpdateRequest.builder() + .tableName("") + .records(records) + .updateType(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.println("data" + record.getTokens()); + 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/main/java/com/skyflow/VaultClient.java b/flowvault/src/main/java/com/skyflow/VaultClient.java index a047e156..68d7c7ae 100644 --- a/flowvault/src/main/java/com/skyflow/VaultClient.java +++ b/flowvault/src/main/java/com/skyflow/VaultClient.java @@ -165,7 +165,7 @@ protected void updateExecutorInHTTP() throws SkyflowException { // mode from this SDK is a SkyflowException, never a raw one. try { OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder() - .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) + .connectionPool(new ConnectionPool(100, 1, TimeUnit.MINUTES)) // Overall ceiling; bounds the whole call including retries. .callTimeout(timeoutSeconds, TimeUnit.SECONDS) // OUTER: retries. Must wrap the auth interceptor so each attempt re-reads the 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/enums/UpdateType.java b/flowvault/src/main/java/com/skyflow/enums/UpdateType.java new file mode 100644 index 00000000..592299c7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/enums/UpdateType.java @@ -0,0 +1,6 @@ +package com.skyflow.enums; + +public enum UpdateType { + UPDATE, + REPLACE +} diff --git a/flowvault/src/main/java/com/skyflow/utils/Constants.java b/flowvault/src/main/java/com/skyflow/utils/Constants.java index bf7da046..3c82cc4b 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Constants.java +++ b/flowvault/src/main/java/com/skyflow/utils/Constants.java @@ -10,23 +10,23 @@ public final class Constants extends BaseConstants { public static final String VAULT_DOMAIN = ".skyvault."; public static final String SDK_PREFIX; public static final String SDK_METRIC_NAME_VERSION_PREFIX = "skyflow-flowvault-java@"; - public static final Integer MAX_BULK_DATA_SIZE = 10000; + public static final Integer MAX_BULK_DATA_SIZE = 100000; public static final Integer INSERT_BATCH_SIZE = 50; public static final Integer MAX_INSERT_BATCH_SIZE = 1000; public static final Integer INSERT_CONCURRENCY_LIMIT = 1; - public static final Integer MAX_INSERT_CONCURRENCY_LIMIT = 10; + public static final Integer MAX_INSERT_CONCURRENCY_LIMIT = 100; public static final Integer DETOKENIZE_BATCH_SIZE = 50; public static final Integer DETOKENIZE_CONCURRENCY_LIMIT = 1; public static final Integer MAX_DETOKENIZE_BATCH_SIZE = 1000; - public static final Integer MAX_DETOKENIZE_CONCURRENCY_LIMIT = 10; + public static final Integer MAX_DETOKENIZE_CONCURRENCY_LIMIT = 100; public static final Integer DELETE_TOKENS_BATCH_SIZE = 50; public static final Integer DELETE_TOKENS_CONCURRENCY_LIMIT = 1; public static final Integer MAX_DELETE_TOKENS_BATCH_SIZE = 1000; - public static final Integer MAX_DELETE_TOKENS_CONCURRENCY_LIMIT = 10; + public static final Integer MAX_DELETE_TOKENS_CONCURRENCY_LIMIT = 100; public static final Integer TOKENIZE_BATCH_SIZE = 50; public static final Integer TOKENIZE_CONCURRENCY_LIMIT = 1; public static final Integer MAX_TOKENIZE_BATCH_SIZE = 1000; - public static final Integer MAX_TOKENIZE_CONCURRENCY_LIMIT = 10; + public static final Integer MAX_TOKENIZE_CONCURRENCY_LIMIT = 100; public static final String DEFAULT_SDK_VERSION = "1.0.0"; public static final String CONTEXT_KEY_REGEX = "^[a-zA-Z0-9_]+$"; diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index cfd8195f..67bf90fc 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -2,66 +2,98 @@ 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; import com.skyflow.enums.Env; +import com.skyflow.enums.UpdateType; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.core.ApiClientApiException; import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; +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.V1ColumnRedactions; +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.V1FlowTokenizeRequestObject; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1GetRequestData; +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.V1TokenGroupRedactions; +import com.skyflow.generated.rest.types.V1UniqueValue; +import com.skyflow.generated.rest.types.V1UpdateRecordData; +import com.skyflow.generated.rest.types.V1UpdateResponse; 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.ColumnRedactions; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; +import com.skyflow.vault.data.DeleteResponseRecord; +import com.skyflow.vault.data.DetokenizeResponseRecordMetadata; +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.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.GetResponseRecord; 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.QueryRequest; +import com.skyflow.vault.data.QueryResponse; +import com.skyflow.vault.data.QueryResponseMetadata; +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.UpdateRequest; +import com.skyflow.vault.data.UpdateRequestRecord; +import com.skyflow.vault.data.UpdateResponse; +import com.skyflow.vault.data.UpdateResponseRecord; 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 +211,123 @@ 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); + + // 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). + UpdateType requestUpdateType = request.getUpdateType(); + if (requestUpdateType != null) { + builder.updateType(FlowEnumUpdateType.valueOf(requestUpdateType.name())); + } + return builder.build(); + } + // ── Bulk (batched/concurrent) request-body builders ────────────────────── // BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way. @@ -186,7 +335,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 +352,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) @@ -379,6 +539,131 @@ public static ErrorRecord createErrorRecord(Map recordMap, int i return err; } + // ── Unary "records"-shaped exception fallback ───────────────────────────── + // + // A unary call's own (only) record can fail outright — e.g. an invalid column on the sole + // record in an update/insert/get/delete request — and the vault reflects that as the overall + // HTTP status, so the generated client throws ApiClientApiException instead of returning a + // normal response body. When the exception body still has the familiar per-record shape + // ({"records": [...]} for insert/update/get/delete, {"response": [...]} for detokenize), the + // failure belongs on the response the same way a 200 partial-success does — not as a thrown + // exception. Each handler below returns null when the body doesn't match that shape, so the + // caller falls back to throwing a SkyflowException as before. + + /** Record maps under {@code key} in an exception body, or null if the shape doesn't match. */ + private static List> extractExceptionRecords(ApiClientApiException apiException, String key) { + Object rawBody = apiException.body(); + if (!(rawBody instanceof Map)) { + return null; + } + Object recordsField = ((Map) rawBody).get(key); + if (!(recordsField instanceof List)) { + return null; + } + List> records = new ArrayList<>(); + for (Object recordObj : (List) recordsField) { + if (recordObj instanceof Map) { + //noinspection unchecked + records.add((Map) recordObj); + } + } + return records.isEmpty() ? null : records; + } + + public static InsertResponse handleInsertRequestException(ApiClientApiException apiException) { + List> recordMaps = extractExceptionRecords(apiException, "records"); + if (recordMaps == null) { + return null; + } + String requestId = extractRequestId(apiException.headers()); + List records = new ArrayList<>(); + for (Map recordMap : recordMaps) { + records.add(new InsertResponseRecord( + readString(recordMap, "tableName"), + readString(recordMap, "skyflowID"), + null, null, null, + readHttpCode(recordMap, apiException.statusCode()), + readErrorMessage(recordMap), + requestId)); + } + return new InsertResponse(records); + } + + public static UpdateResponse handleUpdateRequestException(ApiClientApiException apiException) { + List> recordMaps = extractExceptionRecords(apiException, "records"); + if (recordMaps == null) { + return null; + } + String requestId = extractRequestId(apiException.headers()); + List records = new ArrayList<>(); + for (Map recordMap : recordMaps) { + records.add(new UpdateResponseRecord( + readString(recordMap, "tableName"), + readString(recordMap, "skyflowID"), + null, null, null, + readHttpCode(recordMap, apiException.statusCode()), + readErrorMessage(recordMap), + requestId)); + } + return new UpdateResponse(records); + } + + public static GetResponse handleGetRequestException(ApiClientApiException apiException) { + List> recordMaps = extractExceptionRecords(apiException, "records"); + if (recordMaps == null) { + return null; + } + String requestId = extractRequestId(apiException.headers()); + List records = new ArrayList<>(); + for (Map recordMap : recordMaps) { + records.add(new GetResponseRecord( + readString(recordMap, "tableName"), + readString(recordMap, "skyflowID"), + null, null, null, + readHttpCode(recordMap, apiException.statusCode()), + readErrorMessage(recordMap), + requestId)); + } + return new GetResponse(records); + } + + public static DeleteResponse handleDeleteRequestException(ApiClientApiException apiException) { + List> recordMaps = extractExceptionRecords(apiException, "records"); + if (recordMaps == null) { + return null; + } + String requestId = extractRequestId(apiException.headers()); + List records = new ArrayList<>(); + for (Map recordMap : recordMaps) { + records.add(new DeleteResponseRecord( + readString(recordMap, "skyflowID"), + readHttpCode(recordMap, apiException.statusCode()), + readErrorMessage(recordMap), + requestId)); + } + return new DeleteResponse(records); + } + + public static DetokenizeResponse handleDetokenizeRequestException(ApiClientApiException apiException) { + List> recordMaps = extractExceptionRecords(apiException, "response"); + if (recordMaps == null) { + return null; + } + String requestId = extractRequestId(apiException.headers()); + List records = new ArrayList<>(); + for (Map recordMap : recordMaps) { + records.add(new DetokenizeResponseRecord( + readString(recordMap, "token"), + null, + readString(recordMap, "tokenGroupName"), + null, + readHttpCode(recordMap, apiException.statusCode()), + readErrorMessage(recordMap), + requestId)); + } + return new DetokenizeResponse(records); + } + // Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto // the unified BulkInsertResponseRecord shape that bulk insert now returns. public static List handleBulkInsertBatchException( @@ -743,6 +1028,92 @@ private static String extractBatchErrorMessage(ApiClientApiException apiExceptio return apiException.getMessage(); } + // Unary counterpart of formatBulkInsertResponse: a single, unbatched call has no batch + // index to attach, but the call's own requestId is still populated on error records, + // matching bulk's error != null ? requestId : null convention. + public static InsertResponse formatInsertResponse(V1InsertResponse response, Map> headers) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + String reqID = current.getError().isPresent() ? extractRequestId(headers) : null; + 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), + reqID)); + } + } + return new InsertResponse(records); + } + + // Update has no bulk/batched counterpart, so there is no index to attach here, but the call's + // own requestId is still populated on error records. 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 to attach here, but the call's own + // requestId is still populated on error records. 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, Map> headers) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + String reqID = current.getError().isPresent() ? extractRequestId(headers) : null; + 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), + reqID)); + } + } + return new GetResponse(records); + } + + // Delete has no bulk/batched counterpart, so there is no index to attach here, but the call's + // own requestId is still populated on error records. Unlike insert/update/get, the wire + // response (V1DeleteResponseObject) carries no data/tokens. + public static DeleteResponse formatDeleteResponse(V1DeleteResponse response, Map> headers) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1DeleteResponseObject current : response.getRecords().get()) { + String reqID = current.getError().isPresent() ? extractRequestId(headers) : null; + records.add(new DeleteResponseRecord( + current.getSkyflowId().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null), + reqID)); + } + } + return new DeleteResponse(records); + } + + public static UpdateResponse formatUpdateResponse(V1UpdateResponse response, Map> headers) { + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + for (V1RecordResponseObject current : response.getRecords().get()) { + String reqID = current.getError().isPresent() ? extractRequestId(headers) : null; + 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), + reqID)); + } + } + 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 +1144,28 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo return formattedResponse; } + // Unary counterpart of formatBulkDetokenizeResponse: a single, unbatched call has no batch + // index to attach, but the call's own requestId is still populated on error records, + // matching bulk's error != null ? requestId : null convention. + public static DetokenizeResponse formatDetokenizeResponse(V1FlowDetokenizeResponse response, Map> headers) { + List records = new ArrayList<>(); + if (response != null && response.getResponse().isPresent()) { + for (V1FlowDetokenizeResponseObject current : response.getResponse().get()) { + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(current.getMetadata().orElse(null)); + String reqID = current.getError().isPresent() ? extractRequestId(headers) : 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), + reqID)); + } + } + 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(); @@ -781,7 +1174,7 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { V1FlowDetokenizeResponseObject current = record.get(index); - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(current.getMetadata().orElse(null)); + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(current.getMetadata().orElse(null)); String reqID = null; if(current.getError().isPresent()){ reqID = extractRequestId(headers); @@ -802,6 +1195,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<>(); + QueryResponseMetadata metadata = 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()) { + metadata = new QueryResponseMetadata(response.getMetadata().get().getColumns().orElse(null)); + } + } + return new QueryResponse(records, metadata); + } + 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..d3038e13 100644 --- a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -141,10 +141,12 @@ public static void validateVaultConfiguration(VaultConfig vaultConfig) throws Sk if (vaultUrl.trim().isEmpty()) { LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage()); - } else if (!Utils.isValidUrl(vaultUrl)) { - LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); } + // TEMP: vault URL format validation (https-only) disabled for local mock-server testing. + // else if (!Utils.isValidUrl(vaultUrl)) { + // LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); + // throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); + // } } else if (Utils.getEnvVaultUrl() == null) { if (clusterId == null) { LogUtil.printErrorLog(ErrorLogs.EITHER_VAULT_URL_OR_CLUSTER_ID_REQUIRED.getLog()); @@ -411,6 +413,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 +453,280 @@ 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()); + } + } + } + } + } + + 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..c0ab02cc 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,50 @@ 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(), response.headers()); + LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + // The lone record in a unary request can fail outright, which the vault reflects as + // the overall HTTP status. If the body still carries the usual per-record shape, + // surface it on the response like a 200 partial success would, not as an exception. + InsertResponse fallback = Utils.handleInsertRequestException(e); + if (fallback != null) { + return fallback; + } + 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 +235,50 @@ 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(), response.headers()); + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + // The lone record in a unary request can fail outright, which the vault reflects as + // the overall HTTP status. If the body still carries the usual per-record shape, + // surface it on the response like a 200 partial success would, not as an exception. + DetokenizeResponse fallback = Utils.handleDetokenizeRequestException(e); + if (fallback != null) { + return fallback; + } + 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 +361,50 @@ 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(), response.headers()); + LogUtil.printInfoLog(InfoLogs.DELETE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + // The lone record in a unary request can fail outright, which the vault reflects as + // the overall HTTP status. If the body still carries the usual per-record shape, + // surface it on the response like a 200 partial success would, not as an exception. + DeleteResponse fallback = Utils.handleDeleteRequestException(e); + if (fallback != null) { + return fallback; + } + 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 +570,125 @@ 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(), response.headers()); + LogUtil.printInfoLog(InfoLogs.UPDATE_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + // The lone record in a unary request can fail outright, which the vault reflects as + // the overall HTTP status. If the body still carries the usual per-record shape, + // surface it on the response like a 200 partial success would, not as an exception. + UpdateResponse fallback = Utils.handleUpdateRequestException(e); + if (fallback != null) { + return fallback; + } + 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(), response.headers()); + LogUtil.printInfoLog(InfoLogs.GET_REQUEST_RESOLVED.getLog()); + return formattedResponse; + } catch (ApiClientApiException e) { + // The lone record in a unary request can fail outright, which the vault reflects as + // the overall HTTP status. If the body still carries the usual per-record shape, + // surface it on the response like a 200 partial success would, not as an exception. + GetResponse fallback = Utils.handleGetRequestException(e); + if (fallback != null) { + return fallback; + } + 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/BulkDetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java index 51a93198..362153dc 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java @@ -6,24 +6,21 @@ // in the submitted payload; all other fields are inherited. public class BulkDetokenizeResponseRecord extends DetokenizeResponseRecord { private final int index; - private final String requestId; public BulkDetokenizeResponseRecord(int index, String token, Object value, String tokenGroupName, - DetokenizeMetadata metadata, int httpCode, String error, + DetokenizeResponseRecordMetadata metadata, int httpCode, String error, String requestId) { - super(token, value, tokenGroupName, metadata, httpCode, error); + // requestId is stored on DetokenizeResponseRecord (shared with the unary response), not + // redeclared here — a same-named field on both this class and its parent breaks Gson's + // reflective field walk (see ReflectiveTypeAdapterFactory.getBoundFields). + super(token, value, tokenGroupName, metadata, httpCode, error, requestId); this.index = index; - this.requestId = requestId; } public int getIndex() { return index; } - public String getRequestId() { - return requestId; - } - @Override public String toString() { Gson gson = new Gson().newBuilder().serializeNulls().create(); diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java index 1d85c7b6..832a5ac3 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -1,15 +1,19 @@ package com.skyflow.vault.data; import com.google.gson.Gson; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.logger.LogUtil; import java.util.List; import java.util.Map; -// Bulk counterpart of InsertResponseRecord. Adds the caller-facing position of the record -// in the submitted payload; all other fields are inherited. +// Bulk counterpart of InsertResponseRecord. Extends it to inherit tableName/skyflowId/tokens/ +// data/hashedData/httpCode/error/requestId unchanged; adds only what bulk needs on top: the +// caller-facing index, and the deprecated getFields()/6-arg-constructor back-compat surface that +// only bulk (the pre-existing method) still has to support for its pre-1.0.2 callers — the unary +// InsertResponseRecord is a brand-new type with none of that legacy surface to carry. public class BulkInsertResponseRecord extends InsertResponseRecord { private final int index; - private final String requestId; /** * @deprecated Use {@link #BulkInsertResponseRecord(int, String, String, Map, Map, Map, int, String, String)} @@ -25,17 +29,24 @@ public BulkInsertResponseRecord(int index, String tableName, String skyflowId, public BulkInsertResponseRecord(int index, String tableName, String skyflowId, Map> tokens, Map data, Map hashedData, int httpCode, String error, String requestId) { - super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); + super(tableName, skyflowId, tokens, data, hashedData, httpCode, error, requestId); this.index = index; - this.requestId = requestId; } public int getIndex() { return index; } - public String getRequestId(){ - return requestId; + /** + * @deprecated Response key 'fields' is deprecated. Use {@link #getTokens()} instead. This + * still returns {@code Map}, matching its original (pre-typed) contract — + * see {@link Token#toRawTokens(Map)} for how {@link #getTokens()}'s typed data is rendered + * back into that generic shape. + */ + @Deprecated(since = "1.0.2", forRemoval = true) + public Map getFields() { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_INSERT_FIELDS_GETTER.getLog()); + return Token.toRawTokens(getTokens()); } @Override 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..c478b4e1 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java @@ -0,0 +1,48 @@ +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; + private final String requestId; + + public DeleteResponseRecord(String skyflowId, Integer httpCode, String error) { + this(skyflowId, httpCode, error, null); + } + + public DeleteResponseRecord(String skyflowId, Integer httpCode, String error, String requestId) { + this.skyflowId = skyflowId; + this.httpCode = httpCode; + this.error = error; + this.requestId = requestId; + } + + public String getSkyflowId() { + return skyflowId; + } + + public Integer getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } + + @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/DetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java index b7322c3e..6ee24cb8 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java @@ -4,16 +4,23 @@ public class DetokenizeResponseRecord extends BaseDetokenizeRecordResponse { // Passed straight through from V1FlowDetokenizeResponseObject.getValue() (Optional). private final Object value; private final String tokenGroupName; - private final DetokenizeMetadata metadata; + private final DetokenizeResponseRecordMetadata metadata; private final int httpCode; + private final String requestId; public DetokenizeResponseRecord(String token, Object value, String tokenGroupName, - DetokenizeMetadata metadata, int httpCode, String error) { + DetokenizeResponseRecordMetadata metadata, int httpCode, String error) { + this(token, value, tokenGroupName, metadata, httpCode, error, null); + } + + public DetokenizeResponseRecord(String token, Object value, String tokenGroupName, + DetokenizeResponseRecordMetadata metadata, int httpCode, String error, String requestId) { super(token, error); this.value = value; this.tokenGroupName = tokenGroupName; this.metadata = metadata; this.httpCode = httpCode; + this.requestId = requestId; } public Object getValue() { @@ -26,15 +33,20 @@ public String getTokenGroupName() { /** * The record's skyflowId/tableName, typed. The API models this generically (see - * {@link DetokenizeMetadata#parseMetadata(java.util.Map)}), but the SDK parses it here so - * callers get {@link DetokenizeMetadata#getSkyflowId()}/{@link DetokenizeMetadata#getTableName()} + * {@link DetokenizeResponseRecordMetadata#parseMetadata(java.util.Map)}), but the SDK parses it here so + * callers get {@link DetokenizeResponseRecordMetadata#getSkyflowId()}/{@link DetokenizeResponseRecordMetadata#getTableName()} * directly, with no casting required. */ - public DetokenizeMetadata getMetadata() { + public DetokenizeResponseRecordMetadata getMetadata() { return metadata; } public int getHttpCode() { return httpCode; } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeMetadata.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecordMetadata.java similarity index 81% rename from flowvault/src/main/java/com/skyflow/vault/data/DetokenizeMetadata.java rename to flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecordMetadata.java index d9cfcf3e..2b46a002 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeMetadata.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecordMetadata.java @@ -11,13 +11,13 @@ * (see {@code flowdb_dp_apis.proto}) — {@link #parseMetadata(Map)} normalizes both into the * camelCase, {@code tableName}-shaped accessors below. */ -public class DetokenizeMetadata { +public class DetokenizeResponseRecordMetadata { @Expose(serialize = true) private final String skyflowId; @Expose(serialize = true) private final String tableName; - public DetokenizeMetadata(String skyflowId, String tableName) { + public DetokenizeResponseRecordMetadata(String skyflowId, String tableName) { this.skyflowId = skyflowId; this.tableName = tableName; } @@ -39,16 +39,16 @@ public String toString() { /** * Parses the raw wire-shaped metadata map (keys {@code skyflowID}/{@code skyflowId} and * {@code table}/{@code tableName} — the API has been observed to send either casing) into a - * {@link DetokenizeMetadata}. Returns {@code null} for {@code null} input, matching the + * {@link DetokenizeResponseRecordMetadata}. Returns {@code null} for {@code null} input, matching the * record-level metadata field being absent entirely on error records. */ - public static DetokenizeMetadata parseMetadata(Map rawMetadata) { + public static DetokenizeResponseRecordMetadata parseMetadata(Map rawMetadata) { if (rawMetadata == null) { return null; } Object skyflowId = rawMetadata.containsKey("skyflowId") ? rawMetadata.get("skyflowId") : rawMetadata.get("skyflowID"); Object tableName = rawMetadata.containsKey("tableName") ? rawMetadata.get("tableName") : rawMetadata.get("table"); - return new DetokenizeMetadata( + return new DetokenizeResponseRecordMetadata( skyflowId != null ? skyflowId.toString() : null, tableName != null ? tableName.toString() : null); } 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..76889261 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java @@ -0,0 +1,72 @@ +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), but this does NOT extend +// InsertResponseRecord: doing so would also inherit its deprecated getFields() getter (kept +// there only for insert's pre-1.0.2 back-compat), which a brand-new response type has no +// business exposing. Kept as its own type, matching DeleteResponseRecord's convention. +public class GetResponseRecord { + private final String tableName; + private final String skyflowId; + private final Map> tokens; + private final Map data; + private final Map hashedData; + private final int httpCode; + private final String error; + private final String requestId; + + public GetResponseRecord(String tableName, String skyflowId, Map> tokens, + Map data, Map hashedData, + int httpCode, String error, String requestId) { + this.tableName = tableName; + this.skyflowId = skyflowId; + this.tokens = tokens; + this.data = data; + this.hashedData = hashedData; + this.httpCode = httpCode; + this.error = error; + this.requestId = requestId; + } + + public String getTableName() { + return tableName; + } + + public String getSkyflowId() { + return skyflowId; + } + + /** + * Per-column token data. The API models a column's tokens generically (see + * {@link Token#parseTokens(Map)}), but the SDK parses that into {@link Token} objects here + * so callers get {@link Token#getToken()}/{@link Token#getTokenGroupName()} directly, with no + * casting required. + */ + public Map> getTokens() { + return tokens; + } + + public Map getData() { + return data; + } + + public Map getHashedData() { + return hashedData; + } + + public int getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java index f889437f..1f855b4d 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -1,11 +1,11 @@ package com.skyflow.vault.data; -import com.skyflow.logs.InfoLogs; -import com.skyflow.utils.logger.LogUtil; - import java.util.List; import java.util.Map; +// Response record for the unary insert method. Deliberately does NOT carry the deprecated +// getFields()/6-arg-constructor back-compat surface that BulkInsertResponseRecord still has to — +// this is a brand-new type with no pre-1.0.2 callers to support, so it stays clean. public class InsertResponseRecord { private final String tableName; private final String skyflowId; @@ -14,19 +14,11 @@ public class InsertResponseRecord { private final Map hashedData; private final int httpCode; private final String error; - - /** - * @deprecated Use {@link #InsertResponseRecord(String, String, Map, Map, Map, int, String)} instead, - * which also lets you populate {@code data}. This overload always leaves {@code data} null. - */ - @Deprecated(since = "1.0.2", forRemoval = true) - public InsertResponseRecord(String tableName, String skyflowId, Map> tokens, - Map hashedData, int httpCode, String error) { - this(tableName, skyflowId, tokens, null, hashedData, httpCode, error); - } + private final String requestId; public InsertResponseRecord(String tableName, String skyflowId, Map> tokens, - Map data, Map hashedData, int httpCode, String error) { + Map data, Map hashedData, int httpCode, String error, + String requestId) { this.tableName = tableName; this.skyflowId = skyflowId; this.tokens = tokens; @@ -34,6 +26,7 @@ public InsertResponseRecord(String tableName, String skyflowId, Map> getTokens() { return tokens; } - /** - * @deprecated Response key 'fields' is deprecated. Use {@link #getTokens()} instead. This - * still returns {@code Map}, matching its original (pre-typed) contract — - * see {@link Token#toRawTokens(Map)} for how {@link #getTokens()}'s typed data is rendered - * back into that generic shape. - */ - @Deprecated(since = "1.0.2", forRemoval = true) - public Map getFields() { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_INSERT_FIELDS_GETTER.getLog()); - return Token.toRawTokens(getTokens()); - } - public Map getData() { return data; } @@ -81,4 +62,9 @@ public int getHttpCode() { public String getError() { return error; } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } } 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..8eeaa8b3 --- /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 QueryResponseMetadata metadata; + + public QueryResponse(List records, QueryResponseMetadata metadata) { + this.records = records; + this.metadata = metadata; + } + + public List getRecords() { + return records; + } + + /** Wraps the query's return columns, mirroring the wire shape (metadata.columns) directly. */ + public QueryResponseMetadata getMetadata() { + return metadata; + } + + @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/QueryResponseMetadata.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponseMetadata.java new file mode 100644 index 00000000..7f86e540 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponseMetadata.java @@ -0,0 +1,27 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +// Wraps the query's return columns, mirroring the wire shape (metadata.columns) directly instead +// of a flat columns field bolted onto QueryResponse. +public class QueryResponseMetadata { + private final List columns; + + public QueryResponseMetadata(List columns) { + this.columns = columns; + } + + /** 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..a37f3860 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java @@ -0,0 +1,58 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.UpdateType; + +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; + } + + /** If omitted, the vault treats it the same as {@link UpdateType#UPDATE}. */ + public UpdateType getUpdateType() { + return this.builder.updateType; + } + + public static UpdateRequestBuilder builder() { + return new UpdateRequestBuilder(); + } + + public static class UpdateRequestBuilder { + private String tableName; + private List records; + private UpdateType 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(UpdateType 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..a1c4bb0e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java @@ -0,0 +1,72 @@ +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), but this does NOT extend +// InsertResponseRecord: doing so would also inherit its deprecated getFields() getter (kept +// there only for insert's pre-1.0.2 back-compat), which a brand-new response type has no +// business exposing. Kept as its own type, matching DeleteResponseRecord's convention. +public class UpdateResponseRecord { + private final String tableName; + private final String skyflowId; + private final Map> tokens; + private final Map data; + private final Map hashedData; + private final int httpCode; + private final String error; + private final String requestId; + + public UpdateResponseRecord(String tableName, String skyflowId, Map> tokens, + Map data, Map hashedData, + int httpCode, String error, String requestId) { + this.tableName = tableName; + this.skyflowId = skyflowId; + this.tokens = tokens; + this.data = data; + this.hashedData = hashedData; + this.httpCode = httpCode; + this.error = error; + this.requestId = requestId; + } + + public String getTableName() { + return tableName; + } + + public String getSkyflowId() { + return skyflowId; + } + + /** + * Per-column token data. The API models a column's tokens generically (see + * {@link Token#parseTokens(Map)}), but the SDK parses that into {@link Token} objects here + * so callers get {@link Token#getToken()}/{@link Token#getTokenGroupName()} directly, with no + * casting required. + */ + public Map> getTokens() { + return tokens; + } + + public Map getData() { + return data; + } + + public Map getHashedData() { + return hashedData; + } + + public int getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 3d0ab81d..be3115d8 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -4,22 +4,34 @@ import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; +import com.skyflow.enums.UpdateType; 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 +45,32 @@ 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.DeleteResponseRecord; +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.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; +import com.skyflow.vault.data.GetResponse; +import com.skyflow.vault.data.GetResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; +import com.skyflow.vault.data.InsertResponseRecord; +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 +356,393 @@ 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, new HashMap<>()); + + 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, new HashMap<>()); + + 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, new HashMap<>()); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + @Test + public void testFormatInsertResponse_requestIdOnlyPopulatedOnError() { + Map> headers = new HashMap<>(); + headers.put(Constants.REQUEST_ID_HEADER_KEY, Collections.singletonList("req-insert-1")); + V1RecordResponseObject success = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1RecordResponseObject failure = V1RecordResponseObject.builder().error("failed").build(); + V1InsertResponse response = V1InsertResponse.builder().records(Arrays.asList(success, failure)).build(); + + InsertResponse formatted = Utils.formatInsertResponse(response, headers); + + Assert.assertNull(formatted.getRecords().get(0).getRequestId()); + Assert.assertEquals("req-insert-1", formatted.getRecords().get(1).getRequestId()); + } + + // ── 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, new HashMap<>()); + + 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, new HashMap<>()); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + @Test + public void testFormatDetokenizeResponse_requestIdOnlyPopulatedOnError() { + Map> headers = new HashMap<>(); + headers.put(Constants.REQUEST_ID_HEADER_KEY, Collections.singletonList("req-detok-1")); + V1FlowDetokenizeResponseObject success = V1FlowDetokenizeResponseObject.builder().token("tok-1").build(); + V1FlowDetokenizeResponseObject failure = V1FlowDetokenizeResponseObject.builder() + .token("tok-2").error("failed").build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Arrays.asList(success, failure)).build(); + + DetokenizeResponse formatted = Utils.formatDetokenizeResponse(response, headers); + + Assert.assertNull(formatted.getRecords().get(0).getRequestId()); + Assert.assertEquals("req-detok-1", formatted.getRecords().get(1).getRequestId()); + } + + // ── 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(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, new HashMap<>()); + + 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, new HashMap<>()); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + @Test + public void testFormatUpdateResponse_requestIdOnlyPopulatedOnError() { + Map> headers = new HashMap<>(); + headers.put(Constants.REQUEST_ID_HEADER_KEY, Collections.singletonList("req-update-1")); + V1RecordResponseObject success = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1RecordResponseObject failure = V1RecordResponseObject.builder().error("failed").build(); + V1UpdateResponse response = V1UpdateResponse.builder().records(Arrays.asList(success, failure)).build(); + + UpdateResponse formatted = Utils.formatUpdateResponse(response, headers); + + Assert.assertNull(formatted.getRecords().get(0).getRequestId()); + Assert.assertEquals("req-update-1", formatted.getRecords().get(1).getRequestId()); + } + + // ── 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, new HashMap<>()); + + 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, new HashMap<>()); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + @Test + public void testFormatGetResponse_requestIdOnlyPopulatedOnError() { + Map> headers = new HashMap<>(); + headers.put(Constants.REQUEST_ID_HEADER_KEY, Collections.singletonList("req-get-1")); + V1RecordResponseObject success = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1RecordResponseObject failure = V1RecordResponseObject.builder().error("failed").build(); + V1GetResponse response = V1GetResponse.builder().records(Arrays.asList(success, failure)).build(); + + GetResponse formatted = Utils.formatGetResponse(response, headers); + + Assert.assertNull(formatted.getRecords().get(0).getRequestId()); + Assert.assertEquals("req-get-1", formatted.getRecords().get(1).getRequestId()); + } + + // ── 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, new HashMap<>()); + + 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, new HashMap<>()); + Assert.assertTrue(formatted.getRecords().isEmpty()); + } + + @Test + public void testFormatDeleteResponse_requestIdOnlyPopulatedOnError() { + Map> headers = new HashMap<>(); + headers.put(Constants.REQUEST_ID_HEADER_KEY, Collections.singletonList("req-delete-1")); + 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, headers); + + Assert.assertNull(formatted.getRecords().get(0).getRequestId()); + Assert.assertEquals("req-delete-1", formatted.getRecords().get(1).getRequestId()); + } + + // ── 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.getMetadata().getColumns()); + } + + @Test + public void testFormatQueryResponse_nullResponseReturnsEmptyRecordsAndNullMetadata() { + QueryResponse formatted = Utils.formatQueryResponse(null); + Assert.assertTrue(formatted.getRecords().isEmpty()); + Assert.assertNull(formatted.getMetadata()); + } // ── getBulkInsertRequestBody (bulk overload) ────────────────────────────── @@ -2031,9 +2445,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 @@ -2091,4 +2502,227 @@ public void testDeleteTokensErrorRecord_keepsTheErrorAndEchoedToken() { Assert.assertEquals("Token not found", record.getError()); Assert.assertEquals("tok-1", record.getToken()); } + + // ── handleInsertRequestException / handleUpdateRequestException / handleGetRequestException / + // handleDeleteRequestException / handleDetokenizeRequestException ────────────────────────── + // + // These convert a unary call's ApiClientApiException back into a normal response when the + // body still has the familiar per-record shape. extractExceptionRecords is shared by all five, + // so its null/malformed-input robustness is exercised thoroughly once here (via insert) and + // the remaining handlers each get a focused shape-guard + happy-path check. + + @Test + public void testHandleInsertRequestException_nullBodyReturnsNull() { + ApiClientApiException ex = new ApiClientApiException("boom", 400, null); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_nonMapBodyReturnsNull() { + ApiClientApiException ex = new ApiClientApiException("boom", 400, "plain text body"); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_missingRecordsKeyReturnsNull() { + Map body = new HashMap<>(); + body.put("message", "no records key here"); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_recordsValueNotAListReturnsNull() { + Map body = new HashMap<>(); + body.put("records", "not a list"); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_emptyRecordsListReturnsNull() { + Map body = new HashMap<>(); + body.put("records", new ArrayList<>()); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_recordsWithOnlyNonMapElementsReturnsNull() { + Map body = new HashMap<>(); + body.put("records", Arrays.asList("not-a-map", 123, null)); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleInsertRequestException(ex)); + } + + @Test + public void testHandleInsertRequestException_mixedValidAndInvalidElementsKeepsOnlyValidOnes() { + Map validRecord = new HashMap<>(); + validRecord.put("error", "bad column"); + validRecord.put("httpCode", 400); + Map body = new HashMap<>(); + body.put("records", Arrays.asList("not-a-map", validRecord)); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + + InsertResponse response = Utils.handleInsertRequestException(ex); + Assert.assertNotNull(response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("bad column", response.getRecords().get(0).getError()); + } + + @Test + public void testHandleInsertRequestException_missingHttpCodeFallsBackToExceptionStatusCode() { + Map record = new HashMap<>(); + record.put("error", "bad column"); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 422, body); + + InsertResponse response = Utils.handleInsertRequestException(ex); + Assert.assertNotNull(response); + Assert.assertEquals(422, response.getRecords().get(0).getHttpCode()); + } + + @Test + public void testHandleInsertRequestException_missingErrorFallsBackToUnknownError() { + Map record = new HashMap<>(); + record.put("httpCode", 400); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + + InsertResponse response = Utils.handleInsertRequestException(ex); + Assert.assertNotNull(response); + Assert.assertEquals("Unknown error", response.getRecords().get(0).getError()); + } + + @Test + public void testHandleInsertRequestException_validRecordsShapePopulatesResponseAndRequestId() { + Map record = new HashMap<>(); + record.put("skyflowID", null); + record.put("tableName", "table5"); + record.put("error", "INSERT failed. Column card_number is invalid. Specify a valid column."); + record.put("httpCode", 400); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("Error with status code 400", 400, body); + + InsertResponse response = Utils.handleInsertRequestException(ex); + Assert.assertNotNull(response); + Assert.assertEquals(1, response.getRecords().size()); + InsertResponseRecord result = response.getRecords().get(0); + Assert.assertEquals("table5", result.getTableName()); + Assert.assertNull(result.getSkyflowId()); + Assert.assertEquals("INSERT failed. Column card_number is invalid. Specify a valid column.", result.getError()); + Assert.assertEquals(400, result.getHttpCode()); + } + + @Test + public void testHandleUpdateRequestException_nonRecordsShapeReturnsNull() { + Map errorBody = new HashMap<>(); + errorBody.put("message", "whole request failed"); + Map body = new HashMap<>(); + body.put("error", errorBody); + ApiClientApiException ex = new ApiClientApiException("boom", 404, body); + Assert.assertNull(Utils.handleUpdateRequestException(ex)); + } + + @Test + public void testHandleUpdateRequestException_validRecordsShapePopulatesResponse() { + Map record = new HashMap<>(); + record.put("error", "UPDATE failed. Column card_number is invalid. Specify a valid column."); + record.put("httpCode", 400); + record.put("tableName", ""); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + + UpdateResponse response = Utils.handleUpdateRequestException(ex); + Assert.assertNotNull(response); + Assert.assertEquals("UPDATE failed. Column card_number is invalid. Specify a valid column.", + response.getRecords().get(0).getError()); + Assert.assertEquals(400, response.getRecords().get(0).getHttpCode()); + } + + @Test + public void testHandleGetRequestException_nonRecordsShapeReturnsNull() { + ApiClientApiException ex = new ApiClientApiException("boom", 500, "server error"); + Assert.assertNull(Utils.handleGetRequestException(ex)); + } + + @Test + public void testHandleGetRequestException_validRecordsShapePopulatesResponse() { + Map record = new HashMap<>(); + record.put("skyflowID", "sky-1"); + record.put("tableName", "table1"); + record.put("error", "GET failed. Record not found."); + record.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 404, body); + + GetResponse response = Utils.handleGetRequestException(ex); + Assert.assertNotNull(response); + GetResponseRecord result = response.getRecords().get(0); + Assert.assertEquals("sky-1", result.getSkyflowId()); + Assert.assertEquals("table1", result.getTableName()); + Assert.assertEquals(404, result.getHttpCode()); + } + + @Test + public void testHandleDeleteRequestException_nonRecordsShapeReturnsNull() { + Map body = new HashMap<>(); + body.put("records", "not a list"); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleDeleteRequestException(ex)); + } + + @Test + public void testHandleDeleteRequestException_validRecordsShapePopulatesResponse() { + Map record = new HashMap<>(); + record.put("skyflowID", "sky-1"); + record.put("error", "DELETE failed. Record not found."); + record.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 404, body); + + DeleteResponse response = Utils.handleDeleteRequestException(ex); + Assert.assertNotNull(response); + DeleteResponseRecord result = response.getRecords().get(0); + Assert.assertEquals("sky-1", result.getSkyflowId()); + Assert.assertEquals(Integer.valueOf(404), result.getHttpCode()); + } + + @Test + public void testHandleDetokenizeRequestException_recordsKeyIsWrongShapeForDetokenizeReturnsNull() { + // Detokenize's own wire key is "response", not "records" - a body shaped for the other + // unary ops must not be mistaken for a detokenize failure. + Map record = new HashMap<>(); + record.put("error", "some error"); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 400, body); + Assert.assertNull(Utils.handleDetokenizeRequestException(ex)); + } + + @Test + public void testHandleDetokenizeRequestException_validResponseShapePopulatesResponse() { + Map record = new HashMap<>(); + record.put("token", "tok-1"); + record.put("tokenGroupName", "group1"); + record.put("error", "DETOKENIZE failed. Token not found."); + record.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(record)); + ApiClientApiException ex = new ApiClientApiException("boom", 404, body); + + DetokenizeResponse response = Utils.handleDetokenizeRequestException(ex); + Assert.assertNotNull(response); + DetokenizeResponseRecord result = response.getRecords().get(0); + Assert.assertEquals("tok-1", result.getToken()); + Assert.assertEquals("group1", result.getTokenGroupName()); + Assert.assertEquals("DETOKENIZE failed. Token not found.", result.getError()); + Assert.assertEquals(404, result.getHttpCode()); + } } 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..e5245db2 100644 --- a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -1,33 +1,41 @@ package com.skyflow.utils.validations; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; +import com.skyflow.enums.UpdateType; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.Constants; import com.skyflow.vault.data.BulkDeleteTokensRequest; -import com.skyflow.vault.data.BulkInsertRequestRecord; -import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkDetokenizeRequest; -import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.ColumnRedactions; +import com.skyflow.vault.data.DeleteRequest; import com.skyflow.vault.data.DetokenizeRequest; -import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetRequestRecord; import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertRequestRecord; +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; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class ValidationsTests { private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -293,18 +301,6 @@ public void testValidateVaultConfiguration_emptyVaultUrl() { } } - @Test - public void testValidateVaultConfiguration_invalidVaultUrlFormat() { - VaultConfig config = new VaultConfig(); - config.setVaultId("vault123"); - config.setVaultUrl("http://not-https.example.com"); - try { - Validations.validateVaultConfiguration(config); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertNotNull(e.getMessage()); - } - } @Test public void testValidateVaultConfiguration_validWithClusterId() { @@ -993,13 +989,13 @@ public void testValidateBulkInsertRequest_validRequestWithUpsertAndTokens() { } @Test - public void testValidateBulkInsertRequest_over10000RecordsThrows() { + public void testValidateBulkInsertRequest_overMaxBulkDataSizeRecordsThrows() { // Constants.MAX_BULK_DATA_SIZE is a hard ceiling; batching splits the payload but // does not lift it. Map data = new HashMap<>(); data.put("name", "john"); ArrayList records = new ArrayList<>(); - for (int i = 0; i < 10001; i++) { + for (int i = 0; i < Constants.MAX_BULK_DATA_SIZE + 1; i++) { records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); } BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); @@ -1012,11 +1008,11 @@ public void testValidateBulkInsertRequest_over10000RecordsThrows() { } @Test - public void testValidateBulkInsertRequest_exactly10000RecordsIsValid() { + public void testValidateBulkInsertRequest_exactlyMaxBulkDataSizeRecordsIsValid() { Map data = new HashMap<>(); data.put("name", "john"); ArrayList records = new ArrayList<>(); - for (int i = 0; i < 10000; i++) { + for (int i = 0; i < Constants.MAX_BULK_DATA_SIZE; i++) { records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); } BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); @@ -1028,9 +1024,9 @@ public void testValidateBulkInsertRequest_exactly10000RecordsIsValid() { } @Test - public void testValidateBulkDetokenizeRequest_over10000TokensThrows() { + public void testValidateBulkDetokenizeRequest_overMaxBulkDataSizeTokensThrows() { List tokens = new ArrayList<>(); - for (int i = 0; i < 10001; i++) { + for (int i = 0; i < Constants.MAX_BULK_DATA_SIZE + 1; i++) { tokens.add("token-" + i); } BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); @@ -1043,9 +1039,9 @@ public void testValidateBulkDetokenizeRequest_over10000TokensThrows() { } @Test - public void testValidateBulkDeleteTokensRequest_over10000TokensThrows() { + public void testValidateBulkDeleteTokensRequest_overMaxBulkDataSizeTokensThrows() { List tokens = new ArrayList<>(); - for (int i = 0; i < 10001; i++) { + for (int i = 0; i < Constants.MAX_BULK_DATA_SIZE + 1; i++) { tokens.add("token-" + i); } BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); @@ -1058,9 +1054,9 @@ public void testValidateBulkDeleteTokensRequest_over10000TokensThrows() { } @Test - public void testValidateBulkTokenizeRequest_over10000RecordsThrows() { + public void testValidateBulkTokenizeRequest_overMaxBulkDataSizeRecordsThrows() { ArrayList records = new ArrayList<>(); - for (int i = 0; i < 10001; i++) { + for (int i = 0; i < Constants.MAX_BULK_DATA_SIZE + 1; i++) { records.add(BulkTokenizeRequestRecord.builder() .value("value-" + i) .tokenGroupNames(Collections.singletonList("group")) @@ -1607,7 +1603,612 @@ 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_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(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..870cb5ce 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,348 @@ 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()); + } + + // Regression: a single failing record in a unary call can make the vault reflect the failure + // as the overall HTTP status (here 400), so the generated client throws ApiClientApiException + // instead of returning a normal body. When that exception body still has the familiar + // per-record "records" shape, it must land on the UpdateResponse like a 200 partial success + // would - not surface as a thrown SkyflowException. + @Test + public void testUpdate_recordLevelFailureReflectedAsHttpErrorStillReturnsResponse() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map failedRecord = new HashMap<>(); + failedRecord.put("skyflowID", null); + failedRecord.put("tokens", null); + failedRecord.put("data", null); + failedRecord.put("hashedData", null); + failedRecord.put("error", "UPDATE failed. Column card_number is invalid. Specify a valid column."); + failedRecord.put("httpCode", 400); + failedRecord.put("tableName", ""); + Map responseBody = new HashMap<>(); + responseBody.put("records", Collections.singletonList(failedRecord)); + + when(mockRaw.update(any(), any())) + .thenThrow(new ApiClientApiException("Error with status code 400", 400, responseBody)); + + VaultController controller = createControllerWithMock(mockApi); + + UpdateRequestRecord updateRecord = UpdateRequestRecord.builder().skyflowId("sky-1").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("UPDATE failed. Column card_number is invalid. Specify a valid column.", + response.getRecords().get(0).getError()); + Assert.assertEquals(400, response.getRecords().get(0).getHttpCode()); + Assert.assertNull(response.getRecords().get(0).getSkyflowId()); + } + + // Regression: a genuine whole-request API error (e.g. vault not found) has no "records" key + // at all, so the fallback added above must not swallow it - it still has to throw. + @Test + public void testUpdate_wholeRequestApiErrorStillThrows() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map errorBody = new HashMap<>(); + errorBody.put("grpc_code", 5); + errorBody.put("http_code", 404); + errorBody.put("message", "Invalid request. Vault not found for vaultID: vault123. Specify a valid vaultID."); + errorBody.put("http_status", "Not Found"); + errorBody.put("details", new ArrayList<>()); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorBody); + + when(mockRaw.update(any(), any())) + .thenThrow(new ApiClientApiException("Error with status code 404", 404, responseBody)); + + VaultController controller = createControllerWithMock(mockApi); + + UpdateRequestRecord updateRecord = UpdateRequestRecord.builder().skyflowId("sky-1").build(); + UpdateRequest request = UpdateRequest.builder() + .tableName("table1") + .records(Collections.singletonList(updateRecord)) + .build(); + + try { + controller.update(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals( + "Invalid request. Vault not found for vaultID: vault123. Specify a valid vaultID.", + e.getMessage()); + Assert.assertEquals(404, e.getHttpCode()); + } + } + + @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 +1144,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 // diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java index e7e35cc6..47d6df37 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java @@ -188,7 +188,7 @@ public void testDetokenizeRequest_defaultsAreNull() { @Test public void testDetokenizeResponseRecord_gettersReturnConstructorValues() { - DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1"); + DetokenizeResponseRecordMetadata metadata = new DetokenizeResponseRecordMetadata("skyflow-id-1", "table1"); DetokenizeResponseRecord response = new DetokenizeResponseRecord( "tok-1", "secret-value", "group1", metadata, 200, null); diff --git a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java index b77d704f..3993c8f6 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -14,7 +14,7 @@ * constructor logic or toString() serialization: {@link Token}, * {@link TokenizeResponseRecord}, {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, * {@link DeleteTokensRecord}, {@link BulkDeleteTokensResponseRecord}, - * {@link DeleteTokensSummary}, {@link DetokenizeSummary}, {@link DetokenizeMetadata}, + * {@link DeleteTokensSummary}, {@link DetokenizeSummary}, {@link DetokenizeResponseRecordMetadata}, * {@link ErrorRecord} and {@link DetokenizeResponseObject}. */ public class ResponseComponentTests { @@ -34,7 +34,7 @@ private static Map singleColumnRawFields() { // Tests for Success and Summary were removed: the bulk insert response contract replaced // those classes with BulkInsertResponseRecord / BulkSummary, covered below. Token was removed // in the same rework, then reintroduced (with the same shape it had before) as the type - // InsertResponseRecord.getTokens() now returns - see the InsertResponseRecord section below. + // InsertResponseRecord.getTokens()/BulkInsertResponseRecord.getTokens() now return. // ── BulkInsertResponseRecord ───────────────────────────────────────────── @@ -61,8 +61,6 @@ public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); - // getTokens() is inherited unchanged from InsertResponseRecord - confirm it works on the - // subclass callers actually receive, not just the base class. Assert.assertEquals("tok-1", record.getTokens().get("name").get(0).getToken()); } @@ -86,27 +84,27 @@ public void testBulkInsertResponseRecord_deprecatedConstructorAndGetFieldsStillW } @Test - @SuppressWarnings("deprecation") - public void testInsertResponseRecord_deprecatedConstructorDefaultsDataToNull() { - // BulkInsertResponseRecord's deprecated constructor delegates straight to the new - // 7-arg super constructor, so it never exercises InsertResponseRecord's own deprecated - // 6-arg constructor. Cover that one directly. + public void testInsertResponseRecord_gettersReturnConstructorValues() { + // InsertResponseRecord (unary) carries no deprecated back-compat surface - it is a + // brand-new type with no pre-1.0.2 callers, unlike BulkInsertResponseRecord above. Map> tokens = new HashMap<>(); tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); + Map data = new HashMap<>(); + data.put("name", "john"); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-1"); InsertResponseRecord record = new InsertResponseRecord( - "persons", "skyflow-id-1", tokens, hashedData, 200, null); + "persons", "skyflow-id-1", tokens, data, hashedData, 200, null, "req-1"); Assert.assertEquals("persons", record.getTableName()); Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); Assert.assertEquals(tokens, record.getTokens()); - Assert.assertEquals(singleColumnRawFields(), record.getFields()); - Assert.assertNull(record.getData()); + Assert.assertEquals(data, record.getData()); Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); + Assert.assertEquals("req-1", record.getRequestId()); } @Test @@ -573,7 +571,7 @@ public void testErrorRecord_toStringNotNull() { @Test public void testBulkDetokenizeResponseRecord_gettersReturnConstructorValues() { - DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1"); + DetokenizeResponseRecordMetadata metadata = new DetokenizeResponseRecordMetadata("skyflow-id-1", "table1"); BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( 4, "tok-1", "secret-value", "group1", metadata, 200, null, null); @@ -617,11 +615,11 @@ public void testBulkDetokenizeResponseRecord_toStringSerializesNulls() { Assert.assertTrue(json.contains("\"error\":null")); } - // ── DetokenizeMetadata ──────────────────────────────────────────────────── + // ── DetokenizeResponseRecordMetadata ──────────────────────────────────────────────────── @Test public void testDetokenizeMetadata_gettersReturnConstructorValues() { - DetokenizeMetadata metadata = new DetokenizeMetadata("skyflow-id-1", "table1"); + DetokenizeResponseRecordMetadata metadata = new DetokenizeResponseRecordMetadata("skyflow-id-1", "table1"); Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId()); Assert.assertEquals("table1", metadata.getTableName()); @@ -629,7 +627,7 @@ public void testDetokenizeMetadata_gettersReturnConstructorValues() { @Test public void testDetokenizeMetadata_toStringSerializesFields() { - String json = new DetokenizeMetadata("skyflow-id-1", "table1").toString(); + String json = new DetokenizeResponseRecordMetadata("skyflow-id-1", "table1").toString(); Assert.assertTrue(json.contains("\"skyflowId\":\"skyflow-id-1\"")); Assert.assertTrue(json.contains("\"tableName\":\"table1\"")); @@ -637,7 +635,7 @@ public void testDetokenizeMetadata_toStringSerializesFields() { @Test public void testParseMetadata_returnsNullWhenRawMetadataIsNull() { - Assert.assertNull(DetokenizeMetadata.parseMetadata(null)); + Assert.assertNull(DetokenizeResponseRecordMetadata.parseMetadata(null)); } @Test @@ -648,7 +646,7 @@ public void testParseMetadata_parsesTheCamelCaseWireShape() { raw.put("skyflowId", "skyflow-id-1"); raw.put("table", "table1"); - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw); + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(raw); Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId()); Assert.assertEquals("table1", metadata.getTableName()); @@ -663,7 +661,7 @@ public void testParseMetadata_prefersAnAlreadyCamelCasedTableNameKeyOverTable() raw.put("tableName", "table1"); raw.put("table", "should-be-ignored"); - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw); + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(raw); Assert.assertEquals("table1", metadata.getTableName()); } @@ -676,7 +674,7 @@ public void testParseMetadata_parsesTheLiteralProtoWireShape() { raw.put("skyflowID", "skyflow-id-1"); raw.put("table", "table1"); - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(raw); + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(raw); Assert.assertEquals("skyflow-id-1", metadata.getSkyflowId()); Assert.assertEquals("table1", metadata.getTableName()); @@ -684,7 +682,7 @@ public void testParseMetadata_parsesTheLiteralProtoWireShape() { @Test public void testParseMetadata_missingKeysParseAsNull() { - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(new HashMap<>()); + DetokenizeResponseRecordMetadata metadata = DetokenizeResponseRecordMetadata.parseMetadata(new HashMap<>()); Assert.assertNull(metadata.getSkyflowId()); Assert.assertNull(metadata.getTableName());