diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index b344dd69..1e408cca 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -90,13 +90,13 @@ 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."), @@ -105,7 +105,7 @@ public enum ErrorMessage { 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."), @@ -165,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/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/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/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 51ca252a..c8d72b77 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -12,39 +12,40 @@ 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.V1InsertRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1GetRequest; -import com.skyflow.generated.rest.resources.flowservice.requests.V1DeleteRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.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.V1ExecuteQueryRecordResponse; -import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; 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.V1UpdateRecordData; -import com.skyflow.generated.rest.types.V1ColumnRedactions; -import com.skyflow.generated.rest.types.V1GetRequestData; -import com.skyflow.generated.rest.types.V1GetResponse; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; import com.skyflow.generated.rest.types.V1UniqueValue; -import com.skyflow.generated.rest.types.V1DeleteResponse; -import com.skyflow.generated.rest.types.V1DeleteResponseObject; +import com.skyflow.generated.rest.types.V1UpdateRecordData; import com.skyflow.generated.rest.types.V1UpdateResponse; -import com.skyflow.generated.rest.types.V1TokenGroupRedactions; import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.logs.ErrorLogs; import com.skyflow.utils.logger.LogUtil; @@ -60,33 +61,34 @@ import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.BulkTokenizeResponseRecord; -import com.skyflow.vault.data.DetokenizeMetadata; +import com.skyflow.vault.data.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.InsertRequest; -import com.skyflow.vault.data.InsertRequestRecord; -import com.skyflow.vault.data.InsertResponse; -import com.skyflow.vault.data.InsertResponseRecord; -import com.skyflow.vault.data.UpdateRequest; -import com.skyflow.vault.data.UpdateRequestRecord; -import com.skyflow.vault.data.UpdateResponse; -import com.skyflow.vault.data.UpdateResponseRecord; -import com.skyflow.vault.data.DeleteRequest; -import com.skyflow.vault.data.DeleteResponse; -import com.skyflow.vault.data.DeleteResponseRecord; -import com.skyflow.vault.data.ColumnRedactions; import com.skyflow.vault.data.GetRequest; import com.skyflow.vault.data.GetRequestRecord; import com.skyflow.vault.data.GetResponse; import com.skyflow.vault.data.GetResponseRecord; +import com.skyflow.vault.data.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; @@ -317,29 +319,15 @@ public static V1UpdateRequest getUpdateRequestBody(UpdateRequest request, VaultC .tableName(request.getTableName()) .records(updateRecordDataList); - FlowEnumUpdateType requestUpdateType = resolveUpdateType(request.getUpdateType()); + // 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(requestUpdateType); + builder.updateType(FlowEnumUpdateType.valueOf(requestUpdateType.name())); } return builder.build(); } - // updateType is a String on the request; the legal values come from the wire enum itself - // so there is a single source of truth. Validations rejects anything that does not match. - // Record-level updateType isn't wired here: the generated V1UpdateRecordData has no - // updateType setter yet even though the proto declares one (needs a client regeneration). - private static FlowEnumUpdateType resolveUpdateType(String updateType) { - if (updateType == null) { - return null; - } - for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { - if (type.toString().equalsIgnoreCase(updateType)) { - return type; - } - } - return null; - } - // ── Bulk (batched/concurrent) request-body builders ────────────────────── // BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way. @@ -916,11 +904,13 @@ private static String extractBatchErrorMessage(ApiClientApiException apiExceptio } // Unary counterpart of formatBulkInsertResponse: a single, unbatched call has no batch - // index or requestId to attach, so each record maps straight across with no offset. - public static InsertResponse formatInsertResponse(V1InsertResponse response) { + // 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), @@ -928,22 +918,26 @@ public static InsertResponse formatInsertResponse(V1InsertResponse response) { current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), - current.getError().orElse(null))); + current.getError().orElse(null), + reqID)); } } return new InsertResponse(records); } - // Update has no bulk/batched counterpart, so there is no index or requestId to attach here. - // The wire response reuses V1RecordResponseObject (the same shape as insert's), so the - // per-record mapping mirrors formatInsertResponse. - // Get has no bulk/batched counterpart, so there is no index or requestId to attach here. - // The wire response reuses V1RecordResponseObject (the same shape as insert's/update's), so - // the per-record mapping mirrors formatInsertResponse. - public static GetResponse formatGetResponse(V1GetResponse response) { + // 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), @@ -951,31 +945,36 @@ public static GetResponse formatGetResponse(V1GetResponse response) { current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), - current.getError().orElse(null))); + current.getError().orElse(null), + reqID)); } } return new GetResponse(records); } - // Delete has no bulk/batched counterpart, so there is no index or requestId to attach here. - // Unlike insert/update/get, the wire response (V1DeleteResponseObject) carries no data/tokens. - public static DeleteResponse formatDeleteResponse(V1DeleteResponse response) { + // 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))); + current.getError().orElse(null), + reqID)); } } return new DeleteResponse(records); } - public static UpdateResponse formatUpdateResponse(V1UpdateResponse response) { + 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), @@ -983,7 +982,8 @@ public static UpdateResponse formatUpdateResponse(V1UpdateResponse response) { current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), - current.getError().orElse(null))); + current.getError().orElse(null), + reqID)); } } return new UpdateResponse(records); @@ -1020,19 +1020,22 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo } // Unary counterpart of formatBulkDetokenizeResponse: a single, unbatched call has no batch - // index or requestId to attach, so each record maps straight across with no offset. - public static DetokenizeResponse formatDetokenizeResponse(V1FlowDetokenizeResponse response) { + // 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()) { - DetokenizeMetadata metadata = DetokenizeMetadata.parseMetadata(current.getMetadata().orElse(null)); + 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))); + current.getError().orElse(null), + reqID)); } } return new DetokenizeResponse(records); @@ -1046,7 +1049,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); @@ -1070,7 +1073,7 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni // Query has no batching/bulk counterpart, so there is no index or requestId to attach here. public static QueryResponse formatQueryResponse(V1ExecuteQueryResponse response) { List records = new ArrayList<>(); - List columns = null; + QueryResponseMetadata metadata = null; if (response != null) { if (response.getRecords().isPresent()) { for (V1ExecuteQueryRecordResponse record : response.getRecords().get()) { @@ -1078,10 +1081,10 @@ public static QueryResponse formatQueryResponse(V1ExecuteQueryResponse response) } } if (response.getMetadata().isPresent()) { - columns = response.getMetadata().get().getColumns().orElse(null); + metadata = new QueryResponseMetadata(response.getMetadata().get().getColumns().orElse(null)); } } - return new QueryResponse(records, columns); + return new QueryResponse(records, metadata); } public static BulkDeleteTokensResponse formatBulkDeleteTokensResponse( 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 2c930ac0..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()); @@ -571,16 +573,6 @@ public static void validateUpdateRequest(UpdateRequest updateRequest) throws Sky } } } - - // updateType is a free-form String on the request, but only the wire enum's values reach - // the wire. Reject anything else here rather than silently dropping it during mapping. - String updateType = updateRequest.getUpdateType(); - if (updateType != null && !isKnownUpdateType(updateType)) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.INVALID_UPSERT_UPDATE_TYPE.getLog(), InterfaceName.UPDATE.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidUpsertUpdateType.getMessage()); - } } public static void validateGetRequest(GetRequest getRequest) throws SkyflowException { 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 29eebea8..897c7269 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -138,7 +138,7 @@ public InsertResponse insert(InsertRequest insertRequest, InsertOptions options) ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().insert(request, buildRequestOptions(ctx)); - InsertResponse formattedResponse = Utils.formatInsertResponse(response.body()); + InsertResponse formattedResponse = Utils.formatInsertResponse(response.body(), response.headers()); LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); return formattedResponse; } catch (ApiClientApiException e) { @@ -252,7 +252,7 @@ public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest, Detoke ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().detokenize(request, buildRequestOptions(ctx)); - DetokenizeResponse formattedResponse = Utils.formatDetokenizeResponse(response.body()); + DetokenizeResponse formattedResponse = Utils.formatDetokenizeResponse(response.body(), response.headers()); LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); return formattedResponse; } catch (ApiClientApiException e) { @@ -371,7 +371,7 @@ public DeleteResponse delete(DeleteRequest deleteRequest, DeleteOptions options) ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().delete(request, buildRequestOptions(ctx)); - DeleteResponse formattedResponse = Utils.formatDeleteResponse(response.body()); + DeleteResponse formattedResponse = Utils.formatDeleteResponse(response.body(), response.headers()); LogUtil.printInfoLog(InfoLogs.DELETE_REQUEST_RESOLVED.getLog()); return formattedResponse; } catch (ApiClientApiException e) { @@ -606,7 +606,7 @@ public UpdateResponse update(UpdateRequest updateRequest, UpdateOptions options) ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().update(request, buildRequestOptions(ctx)); - UpdateResponse formattedResponse = Utils.formatUpdateResponse(response.body()); + UpdateResponse formattedResponse = Utils.formatUpdateResponse(response.body(), response.headers()); LogUtil.printInfoLog(InfoLogs.UPDATE_REQUEST_RESOLVED.getLog()); return formattedResponse; } catch (ApiClientApiException e) { @@ -641,7 +641,7 @@ public GetResponse get(GetRequest getRequest, GetOptions options) throws Skyflow ApiClientHttpResponse response = this.getRecordsApi().withRawResponse().get(request, buildRequestOptions(ctx)); - GetResponse formattedResponse = Utils.formatGetResponse(response.body()); + GetResponse formattedResponse = Utils.formatGetResponse(response.body(), response.headers()); LogUtil.printInfoLog(InfoLogs.GET_REQUEST_RESOLVED.getLog()); return formattedResponse; } catch (ApiClientApiException e) { 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/DeleteResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java index 64c6f3e0..c478b4e1 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteResponseRecord.java @@ -10,11 +10,17 @@ 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() { @@ -29,6 +35,11 @@ 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(); 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/GetResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java index 91cd3704..76889261 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/GetResponseRecord.java @@ -4,12 +4,69 @@ import java.util.Map; // The wire shape is identical to InsertResponseRecord (the vault returns the same -// V1RecordResponseObject for get as it does for insert/update), kept as its own type so a -// get response doesn't expose an "Insert*" class name. -public class GetResponseRecord extends InsertResponseRecord { +// 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) { - super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); + 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/QueryResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java index 132cf627..8eeaa8b3 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -7,20 +7,20 @@ public class QueryResponse { private final List records; - private final List columns; + private final QueryResponseMetadata metadata; - public QueryResponse(List records, List columns) { + public QueryResponse(List records, QueryResponseMetadata metadata) { this.records = records; - this.columns = columns; + this.metadata = metadata; } public List getRecords() { return records; } - /** The return columns for the query, when the vault reports them. */ - public List getColumns() { - return columns; + /** Wraps the query's return columns, mirroring the wire shape (metadata.columns) directly. */ + public QueryResponseMetadata getMetadata() { + return metadata; } @Override 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/UpdateRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java index 490d4a3a..a37f3860 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java @@ -1,5 +1,7 @@ package com.skyflow.vault.data; +import com.skyflow.enums.UpdateType; + import java.util.List; public class UpdateRequest { @@ -17,8 +19,8 @@ public List getRecords() { return this.builder.records; } - /** "UPDATE" or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". */ - public String getUpdateType() { + /** If omitted, the vault treats it the same as {@link UpdateType#UPDATE}. */ + public UpdateType getUpdateType() { return this.builder.updateType; } @@ -29,7 +31,7 @@ public static UpdateRequestBuilder builder() { public static class UpdateRequestBuilder { private String tableName; private List records; - private String updateType; + private UpdateType updateType; protected UpdateRequestBuilder() { } @@ -44,7 +46,7 @@ public UpdateRequestBuilder records(List records) { return this; } - public UpdateRequestBuilder updateType(String updateType) { + public UpdateRequestBuilder updateType(UpdateType updateType) { this.updateType = updateType; return 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 index 10777287..a1c4bb0e 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpdateResponseRecord.java @@ -4,12 +4,69 @@ import java.util.Map; // The wire shape is identical to InsertResponseRecord (the vault returns the same -// V1RecordResponseObject for both insert and update), kept as its own type so an update -// response doesn't expose an "Insert*" class name. -public class UpdateResponseRecord extends InsertResponseRecord { +// 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) { - super(tableName, skyflowId, tokens, data, hashedData, httpCode, error); + 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 74325396..80ad7429 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -4,6 +4,7 @@ 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; @@ -361,7 +362,7 @@ public void testFormatInsertResponse_successRecord() { .tableName("table1").skyflowId("sky-id-1").tokens(tokens).build(); V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); - InsertResponse formatted = Utils.formatInsertResponse(response); + InsertResponse formatted = Utils.formatInsertResponse(response, new HashMap<>()); Assert.assertEquals(1, formatted.getRecords().size()); Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); @@ -375,7 +376,7 @@ 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); + InsertResponse formatted = Utils.formatInsertResponse(response, new HashMap<>()); Assert.assertEquals("failed", formatted.getRecords().get(0).getError()); Assert.assertEquals(500, formatted.getRecords().get(0).getHttpCode()); @@ -383,10 +384,24 @@ public void testFormatInsertResponse_errorRecordDefaultsHttpCode500() { @Test public void testFormatInsertResponse_nullResponseReturnsEmptyRecords() { - InsertResponse formatted = Utils.formatInsertResponse(null); + 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 @@ -426,7 +441,7 @@ public void testFormatDetokenizeResponse_successRecord() { V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() .response(Collections.singletonList(record)).build(); - DetokenizeResponse formatted = Utils.formatDetokenizeResponse(response); + DetokenizeResponse formatted = Utils.formatDetokenizeResponse(response, new HashMap<>()); Assert.assertEquals(1, formatted.getRecords().size()); Assert.assertEquals("tok-1", formatted.getRecords().get(0).getToken()); @@ -438,10 +453,26 @@ public void testFormatDetokenizeResponse_successRecord() { @Test public void testFormatDetokenizeResponse_nullResponseReturnsEmptyRecords() { - DetokenizeResponse formatted = Utils.formatDetokenizeResponse(null); + 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 @@ -467,7 +498,7 @@ public void testGetUpdateRequestBody_withRequestLevelUpdateTypeAndRecordTableNam UpdateRequestRecord record = UpdateRequestRecord.builder() .skyflowId("sky-1").data(new HashMap<>()).tableName("table2").build(); UpdateRequest request = UpdateRequest.builder() - .tableName("table1").records(Collections.singletonList(record)).updateType("REPLACE").build(); + .tableName("table1").records(Collections.singletonList(record)).updateType(UpdateType.REPLACE).build(); VaultConfig config = new VaultConfig(); config.setVaultId("vault123"); @@ -498,7 +529,7 @@ public void testFormatUpdateResponse_successRecord() { .tableName("table1").skyflowId("sky-1").build(); V1UpdateResponse response = V1UpdateResponse.builder().records(Collections.singletonList(record)).build(); - UpdateResponse formatted = Utils.formatUpdateResponse(response); + UpdateResponse formatted = Utils.formatUpdateResponse(response, new HashMap<>()); Assert.assertEquals(1, formatted.getRecords().size()); Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); @@ -508,10 +539,24 @@ public void testFormatUpdateResponse_successRecord() { @Test public void testFormatUpdateResponse_nullResponseReturnsEmptyRecords() { - UpdateResponse formatted = Utils.formatUpdateResponse(null); + 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 @@ -565,7 +610,7 @@ public void testFormatGetResponse_successRecord() { .tableName("table1").skyflowId("sky-1").build(); V1GetResponse response = V1GetResponse.builder().records(Collections.singletonList(record)).build(); - GetResponse formatted = Utils.formatGetResponse(response); + GetResponse formatted = Utils.formatGetResponse(response, new HashMap<>()); Assert.assertEquals(1, formatted.getRecords().size()); Assert.assertEquals("table1", formatted.getRecords().get(0).getTableName()); @@ -574,10 +619,24 @@ public void testFormatGetResponse_successRecord() { @Test public void testFormatGetResponse_nullResponseReturnsEmptyRecords() { - GetResponse formatted = Utils.formatGetResponse(null); + 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 @@ -615,7 +674,7 @@ public void testFormatDeleteResponse_successAndErrorRecords() { 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); + DeleteResponse formatted = Utils.formatDeleteResponse(response, new HashMap<>()); Assert.assertEquals(2, formatted.getRecords().size()); Assert.assertEquals("sky-1", formatted.getRecords().get(0).getSkyflowId()); @@ -627,10 +686,24 @@ public void testFormatDeleteResponse_successAndErrorRecords() { @Test public void testFormatDeleteResponse_nullResponseReturnsEmptyRecords() { - DeleteResponse formatted = Utils.formatDeleteResponse(null); + 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 @@ -657,14 +730,14 @@ public void testFormatQueryResponse_withRecordsAndMetadata() { Assert.assertEquals(1, formatted.getRecords().size()); Assert.assertEquals(row, formatted.getRecords().get(0).getData()); - Assert.assertEquals(Collections.singletonList("name"), formatted.getColumns()); + Assert.assertEquals(Collections.singletonList("name"), formatted.getMetadata().getColumns()); } @Test - public void testFormatQueryResponse_nullResponseReturnsEmptyRecordsAndNullColumns() { + public void testFormatQueryResponse_nullResponseReturnsEmptyRecordsAndNullMetadata() { QueryResponse formatted = Utils.formatQueryResponse(null); Assert.assertTrue(formatted.getRecords().isEmpty()); - Assert.assertNull(formatted.getColumns()); + Assert.assertNull(formatted.getMetadata()); } // ── getBulkInsertRequestBody (bulk overload) ────────────────────────────── 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 9e49136e..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,40 +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.GetRequest; import com.skyflow.vault.data.GetRequestRecord; -import com.skyflow.vault.data.InsertRequestRecord; 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"; @@ -300,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() { @@ -1000,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(); @@ -1019,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(); @@ -1035,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(); @@ -1050,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(); @@ -1065,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")) @@ -1933,22 +1922,6 @@ public void testValidateUpdateRequest_blankValueInTokensThrows() { } } - @Test - public void testValidateUpdateRequest_invalidUpdateTypeThrows() { - UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); - UpdateRequest request = UpdateRequest.builder() - .tableName("table1") - .records(Collections.singletonList(record)) - .updateType("NOT_A_REAL_TYPE") - .build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); - } - } - @Test public void testValidateUpdateRequest_validMinimalRequestDoesNotThrow() { UpdateRequestRecord record = UpdateRequestRecord.builder().skyflowId("sky1").build(); @@ -1972,7 +1945,7 @@ public void testValidateUpdateRequest_validWithReplaceUpdateTypeAndRecordTableNa UpdateRequest request = UpdateRequest.builder() .tableName("table1") .records(Collections.singletonList(record)) - .updateType("REPLACE") + .updateType(UpdateType.REPLACE) .build(); try { Validations.validateUpdateRequest(request); 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());