diff --git a/.cspell.json b/.cspell.json index 58d8ce26..cd82e044 100644 --- a/.cspell.json +++ b/.cspell.json @@ -51,6 +51,7 @@ "dotenv", "powermock", "mvn", + "codehaus", "sonatype", "jfrog", "prekarilabs", diff --git a/common/src/main/java/com/skyflow/logs/InfoLogs.java b/common/src/main/java/com/skyflow/logs/InfoLogs.java index 6d6a8211..b4045229 100644 --- a/common/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/common/src/main/java/com/skyflow/logs/InfoLogs.java @@ -115,7 +115,8 @@ public enum InfoLogs { DEPRECATED_UPDATE_LOG_LEVEL("[DEPRECATED] Method 'updateLogLevel()' is deprecated and will be removed in an upcoming release. Use 'setLogLevel()' instead."), DEPRECATED_CREDENTIAL_CLIENT_ID("[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), DEPRECATED_CREDENTIAL_KEY_ID("[DEPRECATED] Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), - DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead.") + DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."), + DEPRECATED_INSERT_FIELDS_GETTER("[DEPRECATED] Method 'getFields()' is deprecated and will be removed in an upcoming release. Use 'getTokens()' instead.") ; diff --git a/flowvault/README.md b/flowvault/README.md index 442a1fbf..da25cb02 100644 --- a/flowvault/README.md +++ b/flowvault/README.md @@ -4,7 +4,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope > Meant for **Flow DB** vaults. -> **`flowvault` is a new SDK, versioned independently of `skyvault`.** It starts at `1.0.0` while `skyvault` (`com.skyflow:skyflow-java`) is at `2.x`. The two artifacts have separate version lines, so a lower `flowvault` version number does not mean it is older or behind — it is a first release, not a downgrade. Upgrade each artifact on its own. +> **`flowvault` is a new SDK, versioned independently of `skyvault`.** It started at `1.0.0` while `skyvault` (`com.skyflow:skyflow-java`) is at `2.x`. The two artifacts have separate version lines, so a lower `flowvault` version number does not mean it is older or behind — it is a first release, not a downgrade. Upgrade each artifact on its own. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) [![License](https://img.shields.io/github/license/skyflowapi/skyflow-java)](https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE) @@ -28,6 +28,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope - [Timeouts and retries](#timeouts-and-retries) - [Logging](#logging) - [VaultController — Bulk operations](#vaultcontroller--bulk-operations) + - [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults) - [Batching and concurrency](#batching-and-concurrency) - [Bulk Insert](#bulk-insert) - [Bulk Tokenize](#bulk-tokenize) @@ -58,7 +59,7 @@ The `flowvault` module is a Skyflow Java SDK built for high-throughput vault ope ### Gradle users ``` -implementation 'com.skyflow:skyflow-flowvault-java:1.0.0' +implementation 'com.skyflow:skyflow-flowvault-java:1.0.1' ``` ### Maven users @@ -67,7 +68,7 @@ implementation 'com.skyflow:skyflow-flowvault-java:1.0.0' com.skyflow skyflow-flowvault-java - 1.0.0 + 1.0.1 ``` @@ -97,7 +98,7 @@ Skyflow skyflowClient = Skyflow.builder() VaultController vault = skyflowClient.vault(); ``` -`flowvault`'s `vault()` takes no arguments — it always resolves to the first vault added to the builder. Use one client per vault if you need to talk to more than one. +`vault()` with no arguments returns the controller for the first vault added to the builder. To talk to more than one vault from a single client, register each with `addVaultConfig(...)` and fetch each controller by ID: `skyflowClient.vault("")`. # Authenticate @@ -249,6 +250,8 @@ public class InitFlowVaultClient { Every method throws `SkyflowException` on validation errors and returns the builder for chaining. +Once built, `skyflowClient.vault()` returns the first registered vault's controller; `skyflowClient.vault("")` returns the controller for a specific registered vault, which is how one client talks to more than one vault. + ## Timeouts and retries Each HTTP setting resolves **most specific first**: the value on `VaultConfig`, else the client-wide value on `Skyflow.builder()`, else the SDK default. Only `null` means "inherit" — an explicit `0` is a real value and overrides the level below it. @@ -297,6 +300,19 @@ The SDK logs through `java.util.logging` at `LogLevel.ERROR` by default. Levels | `bulkDeleteTokens(BulkDeleteTokensRequest)` | `BulkDeleteTokensRequest`, optional `BulkDeleteTokensOptions` | `BulkDeleteTokensResponse` | Delete many tokens in one call | | `bulkDeleteTokensAsync(BulkDeleteTokensRequest)` | same | `CompletableFuture` | Async variant of `bulkDeleteTokens` | +## Schema vs. schemaless vaults + +Which of these operations makes sense depends on whether the vault is **structured** (has a schema — tables and columns) or **schemaless** (stores standalone tokens with no table structure): + +| Operation | Supported on | +|---|---| +| `bulkInsert` / `bulkInsertAsync` | Structured (schema) vaults — inserts into a table's columns. | +| `bulkTokenize` / `bulkTokenizeAsync` | Schemaless vaults — tokenizes a raw value directly against named token groups, with no table involved. | +| `bulkDeleteTokens` / `bulkDeleteTokensAsync` | Schemaless vaults. | +| `bulkDetokenize` / `bulkDetokenizeAsync` | Both — detokenizing only needs the token itself, not a table, so it works regardless of which kind of vault the token came from. | + +This reflects supported use cases, not something the SDK validates or blocks — nothing stops you from calling, say, `bulkTokenize` against a structured vault; it just isn't the intended usage and isn't a scenario the SDK is tested against. + Each method also accepts an optional options object (`BulkInsertOptions`, `BulkTokenizeOptions`, `BulkDetokenizeOptions`, `BulkDeleteTokensOptions`) — see [Custom Request Headers](#custom-request-headers). A single bulk call accepts at most **10,000** records or tokens; anything larger is rejected up front with a `SkyflowException`. Under that ceiling the SDK splits the payload into batches and sends them concurrently, which is why errors from one call can carry different `requestId` values. @@ -342,11 +358,13 @@ The 10,000-item ceiling per bulk call is a separate, fixed limit and is not conf 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`. +> **Vault type supported:** structured (schema) vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + **Note:** - `tableName` must be specified at exactly one level: either on the request (`BulkInsertRequest.builder().tableName(...)`) or on **every** record (`BulkInsertRequestRecord.builder().tableName(...)`) — not both, and not neither. - `upsert` is optional, but wherever you supply it, it must sit at the same level as `tableName`. Request-level `tableName` pairs with request-level `upsert`; record-level `tableName` pairs with per-record `upsert`. -- `UpsertOptions` requires `uniqueColumns`. `updateType` accepts `"UPDATE"` (the default) or `"REPLACE"`. +- `UpsertOptions` requires `uniqueColumns`. `updateType` accepts `"UPDATE"` or `"REPLACE"` — if omitted, the SDK sends no `updateType` at all, and the vault treats that the same as `"UPDATE"`. ### Construct a bulk insert request @@ -435,8 +453,17 @@ Sample response: "requestId": null, "tableName": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "fields": { "card_number": "5484-7829-1702-9110", "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" }, - "hashedData": null, + "tokens": { + "card_number": [ + { "token": "5484-7829-1702-9110", "tokenGroupName": "card_number_cg" } + ], + "cardholder_name": [ + { "token": "b2308e2a-c1f5-469b-97b7-1f193159399b", "tokenGroupName": "deterministic_string" }, + { "token": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "tokenGroupName": "vault_token_group" } + ] + }, + "data": { "card_number": "4111-1111-1111-1111", "cardholder_name": "John Doe" }, + "hashedData": { "card_number": "b6e6d...c3f9" }, "httpCode": 200, "error": null }, @@ -445,7 +472,8 @@ Sample response: "requestId": "a1b2c3d4-...", "tableName": "table2", "skyflowId": null, - "fields": null, + "tokens": null, + "data": null, "hashedData": null, "httpCode": 400, "error": "Insert failed. Column email is invalid." @@ -454,7 +482,19 @@ Sample response: } ``` -Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getFields()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. +`getTokens()` returns `Map>` — one entry per token group configured on that column, so a column with a single token group still comes back as a one-element list, not a bare string. On the wire the API models this generically (`Object`, not a fixed type) to stay flexible, but the SDK parses it into `Token` objects before handing it back, so callers get `Token.getToken()`/`Token.getTokenGroupName()` directly with no casting required: + +```java +for (Token token : record.getTokens().get("card_number")) { + System.out.println(token.getTokenGroupName() + " -> " + token.getToken()); +} +``` + +The parser (`Token.parseTokens()`) normalizes every shape the raw wire value is known to take — a list of `{token, tokenGroupName}` entries, a single such entry not wrapped in a list, or a bare token value with no group information — into a consistent `List`, rather than throwing on an unexpected one. `getTokens()` returns `null` when the record has no tokens (e.g. a failed record). + +Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getTokens()`, `getData()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +> **Deprecation notice:** `getFields()` is deprecated in favor of `getTokens()` — it is kept only for backward compatibility and will be removed in a future release. Update call sites to `getTokens()`. Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequestRecord`s worth resubmitting — see [Retrying the failed records](#retrying-the-failed-records). @@ -462,6 +502,8 @@ Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequest Tokenize many values in one call. Each value can be tokenized against one or more named token groups. +> **Vault type supported:** schemaless vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk tokenize request ```java @@ -538,6 +580,8 @@ Tokenize reports at **two** levels: one entry per input value in `records`, and Detokenize many tokens in one call, optionally overriding the redaction applied per token group via `tokenGroupRedactions`. +> **Vault type supported:** both. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk detokenize request ```java @@ -592,7 +636,7 @@ Sample response: "requestId": null, "value": "4111111111111111", "tokenGroupName": "card_number_cg", - "metadata": {}, + "metadata": { "table": "table1", "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1" }, "httpCode": 200, "token": "5479-4229-4622-1393", "error": null @@ -617,6 +661,8 @@ Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth re Delete many tokens in one call. +> **Vault type supported:** schemaless vaults. See [Schema vs. schemaless vaults](#schema-vs-schemaless-vaults). + ### Construct a bulk delete tokens request ```java @@ -671,16 +717,16 @@ To include custom HTTP headers on an outgoing bulk request, pass a `RequestInter | `CustomHeaderKey` | HTTP header name | |---|---| -| `SkyflowAccountId` | `x-skyflow-account-id` | -| `SkyflowAccountName` | `x-skyflow-account-name` | -| `RequestIdHeader` | `x-request-id` | +| `SKYFLOW_ACCOUNT_ID` | `x-skyflow-account-id` | +| `SKYFLOW_ACCOUNT_NAME` | `x-skyflow-account-name` | +| `REQUEST_ID_HEADER` | `x-request-id` | ```java import com.skyflow.enums.CustomHeaderKey; import com.skyflow.vault.data.BulkInsertOptions; BulkInsertOptions options = BulkInsertOptions.builder() - .interceptor(context -> context.addHeader(CustomHeaderKey.RequestIdHeader, "")) + .interceptor(context -> context.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "")) .build(); BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest, options); @@ -721,7 +767,7 @@ Every bulk response exposes `getSummary()` and `getRecords()`. The records list | `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. | -The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getFields()` for insert, `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. +The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getTokens()`/`getData()` for insert (`getFields()` is a deprecated alias for `getTokens()`), `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. Summaries per operation: @@ -805,7 +851,7 @@ vault.bulkInsertAsync(insertRequest) |---|---|---| | HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). | | Message | `getMessage()` | Human-readable description of the error. | -| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). | +| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"Bad Request"` for a client-side validation error; for API errors, whatever string the server returns). | | gRPC code | `getGrpcCode()` | gRPC status code from the server. | | Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. | | Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. | diff --git a/flowvault/api-report/skyflow-flowvault-java.baseline.jar b/flowvault/api-report/skyflow-flowvault-java.baseline.jar index d98e0eb7..021d9cdb 100644 Binary files a/flowvault/api-report/skyflow-flowvault-java.baseline.jar and b/flowvault/api-report/skyflow-flowvault-java.baseline.jar differ diff --git a/flowvault/samples/README.md b/flowvault/samples/README.md index 666b2186..5f62a8fc 100644 --- a/flowvault/samples/README.md +++ b/flowvault/samples/README.md @@ -1,84 +1,75 @@ -# Java SDK samples -Test the SDK by adding `VAULT-ID`, `VAULT-URL`, and `SERVICE-ACCOUNT` details in the required places for each sample. +# FlowVault Java SDK samples + +Runnable samples for the `flowvault` module (`com.skyflow:skyflow-flowvault-java`) — bulk vault +operations against a Flow DB vault, plus the shared service-account/bearer-token utilities. + +See the [flowvault README](../README.md) for the full API reference these samples exercise. ## Prerequisites -- A Skyflow account. If you don't have one, register for one on the [Try Skyflow](https://skyflow.com/try-skyflow) page. -- Java 1.8 or higher. -### Create the vault +- A Skyflow account. If you don't have one, register on the [Try Skyflow](https://skyflow.com/try-skyflow) page. +- Java 8 or higher, and Maven. + +### Create a vault + 1. In a browser, sign in to Skyflow Studio. -2. Create a vault by clicking **Create Vault** > **Start With a Template** > **Quickstart vault**. +2. Create a vault: **Create Vault** > **Start With a Template** > **Quickstart vault**. 3. Once the vault is ready, click the gear icon and select **Edit Vault Details**. -4. Note your **Vault URL** and **Vault ID** values, then click **Cancel**. You'll need these later. +4. Note the **Vault ID** and **Vault URL**. The cluster ID that samples pass to `setClusterId(...)` + is the subdomain of the vault URL — e.g. for `https://a1b2c3d4.vault.skyflowapis.com`, the + cluster ID is `a1b2c3d4`. ### Create a service account -1. In the side navigation click, **IAM** > **Service Accounts** > **New Service Account**. -2. For **Name**, enter "SDK Sample". For **Roles**, choose **Vault Editor**. -3. Click **Create**. Your browser downloads a **credentials.json** file. Keep this file secure, as You'll need it for each of the samples. + +1. In the side navigation: **IAM** > **Service Accounts** > **New Service Account**. +2. Name it, and for **Roles** choose **Vault Editor**. +3. Click **Create**. Your browser downloads a `credentials.json` — keep it secure, you'll need its + path or contents for most samples below. + +## Running a sample + +Each sample is a `public class X { public static void main(String[] args) ... }` under +`src/main/java/com/example/`. Fill in the placeholders (``, ``, +``, etc.) with real values, then compile and run with Maven from +`flowvault/samples/`: + +```bash +mvn compile +mvn org.codehaus.mojo:exec-maven-plugin:3.1.0:java -Dexec.mainClass="com.example.vault.BulkInsertSync" +``` + +Swap `-Dexec.mainClass` for the fully-qualified class name of the sample you want to run. ## The samples -### Detokenize -Detokenize a data token from the vault. Make sure the specified token is for data that exists in the vault. If you need a valid token, use [InsertExample.java](src/main/java/com/example/InsertExample.java) to insert the data, then use this data's token for detokenization. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/DetokenizeExample.java) -1. Replace **** with **VAULT ID** -2. Replace **** with **VAULT URL** -3. Replace **** with **Data Token**. -4. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**.See #Create a service account. -#### Run the sample - - javac DetokenizeExample.java - java DetokenizeExample -### Get a record by ID -Get data using skyflow id. -#### Configure -1. Replace **** with **VAULT ID** -2. Replace **** with **VAULT URL**. -3. Replace **** with **Skyflow id**. -4. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. See #Create a Service Account. -5. Replace **** with **credit_cards**. -#### Run the sample - - javac GetByIdExample.java - java GetByIdExample -### Insert data into a vault -Insert data in the vault. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/InsertExample.java) -1. Replace **** with **VAULT ID**. -2. Replace **** with **VAULT URL**. -3. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. -4. Replace **** with **credit_cards**. -5. Replace **** with **column name**. -6. Replace **** with **valid value corresponding to column name**. -#### Run the sample - - javac InsertExample.java - java InsertExample -### Invoke a connection -Skyflow Connections is a gateway service that uses Skyflow's underlying tokenization capabilities to securely connect to first-party and third-party services. This way, your infrastructure is never directly exposed to sensitive data, and you offload security and compliance requirements to Skyflow. -#### Configure -1. Replace **** with **VAULT ID**. -2. Replace **** with **VAULT URL**. -3. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE**. -4. Replace **** with **Connection url**. -5. Replace **** with **Path param key**. -6. Replace **** with **Path param value**. -7. Replace **** with **Query param key**. -8. Replace **** with **Query param value**. -9. Replace **** with **Request header key**. -10. Replace **** with **Request header value**. -11. Replace **** with **Request body key**. -12. Replace **** with **Request body value**. -#### Run the sample - - javac InvokeConnectionExample.java - java InvokeConnectionExample - -### Generate a service account bearer token -Generates a bearer token using a file path and content of a service account credentials file. -#### [Configure](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/TokenGenerationExample.java) -1. Replace **** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE PATH**. See #Create a service account. -2. Replace **<>** with relative path of **SERVICE ACCOUNT CREDENTIAL FILE CONTENT AS STRING**. -#### Run the sample - - javac TokenGenerationExample.java - java TokenGenerationExample + +### Service account (`com.example.serviceaccount`) + +These use the `BearerToken` / `SignedDataTokens` utilities directly, for callers who want to +manage their own bearer tokens rather than passing a credentials file/string straight to +`Credentials` (see [Authenticate](../README.md#authenticate) for when you'd do which). + +| Sample | Demonstrates | +|---|---| +| [BearerTokenGenerationExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java) | Basic token generation from a credentials file or credentials string, using `Token.isExpired()` to avoid re-minting a still-valid token. | +| [BearerTokenGenerationUsingThreadsExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java) | Sharing one `BearerToken` instance safely across multiple threads. | +| [BearerTokenGenerationWithContextExample.java](src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java) | Context-aware tokens via `setCtx(String)` and `setCtx(Map)`, for CEL policies keyed on request context. | +| [ScopedTokenGenerationExample.java](src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java) | Scoped tokens via `setRoles(...)`, restricting the token to specific role IDs. | +| [SignedTokenGenerationExample.java](src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java) | Signed data tokens via `SignedDataTokens`, with string and object context variants. | + +### Vault operations (`com.example.vault`) + +| Sample | Demonstrates | +|---|---| +| [BulkInsertSync.java](src/main/java/com/example/vault/BulkInsertSync.java) / [BulkInsertAsync.java](src/main/java/com/example/vault/BulkInsertAsync.java) | Bulk insert into one table (`tableName` at the request level), reading the summary, walking per-record results, and retrying failed records. | +| [BulkMultiTableInsertSync.java](src/main/java/com/example/vault/BulkMultiTableInsertSync.java) / [BulkMultiTableInsertAsync.java](src/main/java/com/example/vault/BulkMultiTableInsertAsync.java) | Bulk insert across multiple tables in one call (`tableName` set per record instead). | +| [BulkTokenizeSync.java](src/main/java/com/example/vault/BulkTokenizeSync.java) / [BulkTokenizeAsync.java](src/main/java/com/example/vault/BulkTokenizeAsync.java) | Tokenizing values against one or more token groups, reading the per-group outcome (a value can partially succeed), and retrying. | +| [BulkDetokenizeSync.java](src/main/java/com/example/vault/BulkDetokenizeSync.java) / [BulkDetokenizeAsync.java](src/main/java/com/example/vault/BulkDetokenizeAsync.java) | Detokenizing tokens with a per-token-group redaction override, reading per-token results, and retrying. | +| [BulkDeleteTokensSync.java](src/main/java/com/example/vault/BulkDeleteTokensSync.java) / [BulkDeleteTokensAsync.java](src/main/java/com/example/vault/BulkDeleteTokensAsync.java) | Deleting tokens, reading per-token results, and retrying. | +| [CustomHeaderExample.java](src/main/java/com/example/vault/CustomHeaderExample.java) | Attaching a custom HTTP header (e.g. a request id) to outgoing requests via a `RequestInterceptor` on the operation's options object. | +| [TimeoutAndRetryConfigExample.java](src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java) | Configuring HTTP timeouts and transport-level retry behavior, per-vault and client-wide, and how the two levels resolve. | + +Every `*Sync`/`*Async` pair above shows the same request-building and response-handling logic; the +`Async` variant just wraps it in `CompletableFuture` callbacks instead of a direct call. Response +handling follows the same shape everywhere: read `getSummary()` for totals, then walk `getRecords()` +checking `getError() == null` per entry — see [Error Handling](../README.md#error-handling) for the +full model these samples are built on. \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java deleted file mode 100644 index 84e8ff55..00000000 --- a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.example.serviceaccount; - -import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; -import com.skyflow.enums.LogLevel; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.BulkDetokenizeRequest; -import com.skyflow.vault.data.BulkDetokenizeResponse; - -import java.util.ArrayList; -import java.util.List; - -/** - * This example demonstrates how to configure and use the Skyflow SDK - * to detokenize sensitive data stored in a Skyflow vault. - * It includes setting up credentials, configuring the vault, and - * making a bulk detokenization request. The code also implements a retry - * mechanism to handle unauthorized access errors (HTTP 401), e.g. when - * the bearer token minted from the credentials has expired. - */ -public class BearerTokenExpiryExample { - public static void main(String[] args) { - try { - // Setting up credentials for accessing the Skyflow vault - Credentials vaultCredentials = new Credentials(); - vaultCredentials.setCredentialsString(""); - - // Configuring the Skyflow vault with necessary details - VaultConfig vaultConfig = new VaultConfig(); - vaultConfig.setVaultId(""); // Vault ID - vaultConfig.setClusterId(""); // Cluster ID - vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) - vaultConfig.setCredentials(vaultCredentials); // Setting credentials - - // Creating a Skyflow client instance with the configured vault - Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR - .addVaultConfig(vaultConfig) // Adding vault configuration - .build(); - - // Attempting to detokenize data using the Skyflow client - try { - detokenizeData(skyflowClient); - } catch (SkyflowException e) { - // Retry detokenization if the error is due to unauthorized access (HTTP 401) - if (e.getHttpCode() == 401) { - detokenizeData(skyflowClient); - } else { - // Rethrow the exception for other error codes - throw e; - } - } - } catch (SkyflowException e) { - // Handling any exceptions that occur during the process - System.out.println("An error occurred: " + e.getMessage()); - } - } - - /** - * Method to detokenize data using the Skyflow client. - * It sends a bulk detokenization request with a list of tokens and prints the response. - * - * @param skyflowClient The Skyflow client instance used for detokenization. - * @throws SkyflowException If an error occurs during the detokenization process. - */ - public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { - // Creating a list of tokens to be detokenized - List tokens = new ArrayList<>(); - tokens.add(""); // First token - tokens.add(""); // Second token - - // Building a bulk detokenization request with the token list - BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() - .tokens(tokens) // Adding tokens to the request - .build(); - - // Sending the detokenization request and receiving the response - BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); - - // Printing the detokenized response - System.out.println(detokenizeResponse); - } -} diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java index c6354fe8..d5a26679 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java @@ -8,6 +8,7 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; import com.skyflow.vault.data.TokenGroupRedactions; import java.util.ArrayList; @@ -22,7 +23,8 @@ * 2. Creating a list of tokens to detokenize * 3. Configuring token group redactions * 4. Building and executing an async bulk detokenize request - * 5. Handling the detokenize response or errors using CompletableFuture + * 5. Reading the per-token outcome and the summary from the response + * 6. Handling the detokenize response or errors using CompletableFuture */ public class BulkDetokenizeAsync { @@ -46,7 +48,9 @@ public static void main(String[] args) { .addVaultConfig(vaultConfig) .build(); - // Step 4: Prepare list of tokens to detokenize + // Step 4: Prepare list of tokens to detokenize. The SDK assigns each token an index + // from its position in this list and returns it on the matching response record, so + // results stay correlated even though the batches complete out of order. List tokens = new ArrayList<>(); tokens.add(""); tokens.add(""); @@ -70,13 +74,37 @@ public static void main(String[] args) { skyflowClient.vault().bulkDetokenizeAsync(detokenizeRequest); future.thenAccept(response -> { System.out.println("Async bulk detokenize resolved with response:\t" + response); + + // Read the summary. totalTokens counts the tokens you submitted, and the other + // two classify each one, so together they sum to that count. + System.out.println("total tokens:\t" + response.getSummary().getTotalTokens()); + System.out.println("detokenized:\t" + response.getSummary().getTotalDetokenized()); + System.out.println("failed:\t\t" + response.getSummary().getTotalFailed()); + + // Walk the per-token outcomes. A record succeeded when its error is null; + // requestId identifies the batch an error came from and is set on failures only. + for (BulkDetokenizeResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> value=%s group=%s%n", + record.getIndex(), record.getToken(), record.getValue(), record.getTokenGroupName()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Tokens that failed with a retryable status (5xx other than 529) can be resubmitted + if (!response.getTokensToRetry().isEmpty()) { + System.out.println("tokens to retry:\t" + response.getTokensToRetry()); + } }).exceptionally(throwable -> { System.err.println("Async bulk detokenize rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); - } catch (SkyflowException e) { + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints + } catch (Exception e) { // Step 8: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java index 957c23ff..8f75257e 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java @@ -8,6 +8,7 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; import com.skyflow.vault.data.TokenGroupRedactions; import java.util.ArrayList; @@ -20,7 +21,8 @@ * 2. Creating a list of tokens to detokenize * 3. Configuring token group redactions * 4. Building and executing a bulk detokenize request - * 5. Handling the detokenize response or any potential errors + * 5. Reading the per-token outcome and the summary from the response + * 6. Handling the detokenize response or any potential errors */ public class BulkDetokenizeSync { @@ -44,7 +46,10 @@ public static void main(String[] args) { .addVaultConfig(vaultConfig) .build(); - // Step 4: Prepare list of tokens to detokenize + // Step 4: Prepare list of tokens to detokenize. The SDK assigns each token an index + // from its position in this list and returns it on the matching response record, so + // results stay correlated even though large requests are split into batches that run + // concurrently. List tokens = new ArrayList<>(); tokens.add(""); tokens.add(""); @@ -66,9 +71,41 @@ public static void main(String[] args) { // Step 7: Execute the bulk detokenize operation and print the response BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); System.out.println(detokenizeResponse); + + // Step 8: Read the summary. totalTokens counts the tokens you submitted, and the + // other two classify each one, so together they sum to that count. + System.out.println("total tokens:\t" + detokenizeResponse.getSummary().getTotalTokens()); + System.out.println("detokenized:\t" + detokenizeResponse.getSummary().getTotalDetokenized()); + System.out.println("failed:\t\t" + detokenizeResponse.getSummary().getTotalFailed()); + + // Step 9: Walk the per-token outcomes. A record succeeded when its error is null; + // requestId identifies the batch an error came from and is set on failures only. + for (BulkDetokenizeResponseRecord record : detokenizeResponse.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> value=%s group=%s%n", + record.getIndex(), record.getToken(), record.getValue(), record.getTokenGroupName()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Step 10: Optionally retry the tokens that failed with a retryable status (5xx other + // than 529). A token that simply does not exist fails with a 4xx, so it is not included. + List tokensToRetry = detokenizeResponse.getTokensToRetry(); + if (!tokensToRetry.isEmpty()) { + System.out.println("retrying:\t" + tokensToRetry); + BulkDetokenizeResponse retryResponse = skyflowClient.vault().bulkDetokenize( + BulkDetokenizeRequest.builder() + .tokens(tokensToRetry) + .tokenGroupRedactions(tokenGroupRedactions) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 8: Handle any errors that occur during the process + // Step 11: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java index dff0deca..791a74dd 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java @@ -8,12 +8,15 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -23,7 +26,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing an async bulk insert request - * 4. Handling the insert response or errors using CompletableFuture + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or errors using CompletableFuture */ public class BulkInsertAsync { @@ -73,7 +77,7 @@ public static void main(String[] args) { insertRecords.add(insertRecord2); // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,16 +96,45 @@ public static void main(String[] args) { // Step 9: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Records that failed with a retryable status (5xx other than 529) come back + // unchanged and can be resubmitted as-is. + if (!response.getRecordsToRetry().isEmpty()) { + System.out.println("records to retry:\t" + response.getRecordsToRetry().size()); + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 10: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java index c3ff5d60..8f146a82 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java @@ -9,12 +9,15 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. @@ -22,7 +25,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing a bulk insert request - * 4. Handling the insert response or any potential errors + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or any potential errors */ public class BulkInsertSync { @@ -72,7 +76,7 @@ public static void main(String[] args) { insertRecords.add(insertRecord2); // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,9 +96,46 @@ public static void main(String[] args) { // Step 9: Execute the bulk insert operation and print the response BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); System.out.println(response); + + // Step 10: Read the summary, then walk the per-record outcomes. A record succeeded + // when its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Step 11: Optionally retry the records that failed with a retryable status (5xx other + // than 529). Your original records come back unchanged and can be resubmitted as-is. + List recordsToRetry = response.getRecordsToRetry(); + if (!recordsToRetry.isEmpty()) { + BulkInsertResponse retryResponse = skyflowClient.vault().bulkInsert( + BulkInsertRequest.builder() + .tableName("") + .upsert(upsert) + .records(new ArrayList<>(recordsToRetry)) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 10: Handle any errors that occur during the process + // Step 12: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java index c0f15834..3dbfccbe 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -8,12 +8,15 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -23,7 +26,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing an async bulk insert request - * 4. Handling the insert response or errors using CompletableFuture + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or errors using CompletableFuture * *

Multi-table mode: the table name is set on every record instead of on the request. * The SDK rejects a request that sets it at both levels, or on only some of the records. @@ -58,7 +62,7 @@ public static void main(String[] args) { List upsertColumns = new ArrayList<>(); upsertColumns.add(""); - // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // upsert is optional; when updateType is omitted the vault treats it the same as "UPDATE". // Set .updateType("REPLACE") to replace the matched row instead. UpsertOptions upsert = UpsertOptions.builder() .uniqueColumns(upsertColumns) @@ -94,16 +98,45 @@ public static void main(String[] args) { // Step 8: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Records that failed with a retryable status (5xx other than 529) come back + // unchanged and can be resubmitted as-is. + if (!response.getRecordsToRetry().isEmpty()) { + System.out.println("records to retry:\t" + response.getRecordsToRetry().size()); + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 9: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java index a3c1211b..ddc835ac 100644 --- a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -9,12 +9,15 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. @@ -22,7 +25,8 @@ * 1. Setting up credentials and vault configuration * 2. Creating multiple records to be inserted * 3. Building and executing a bulk insert request - * 4. Handling the insert response or any potential errors + * 4. Reading the per-record outcome and the summary from the response + * 5. Handling the insert response or any potential errors * *

Multi-table mode: the table name is set on every record instead of on the request. * The SDK rejects a request that sets it at both levels, or on only some of the records. @@ -57,7 +61,7 @@ public static void main(String[] args) { List upsertColumns = new ArrayList<>(); upsertColumns.add(""); - // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // upsert is optional; when updateType is omitted the vault treats it the same as "UPDATE". // Set .updateType("REPLACE") to replace the matched row instead. UpsertOptions upsert = UpsertOptions.builder() .uniqueColumns(upsertColumns) @@ -94,8 +98,44 @@ public static void main(String[] args) { // Step 8: Execute the bulk insert operation and print the response BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); System.out.println(response); + + // Step 9: Read the summary, then walk the per-record outcomes. A record succeeded + // when its error is null; requestId identifies the batch an error came from and is + // set on failures only. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s -> skyflowId=%s%n", + record.getIndex(), record.getTableName(), record.getSkyflowId()); + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } + + // Step 10: Optionally retry the records that failed with a retryable status (5xx other + // than 529). Each returned record still carries its own tableName/upsert, so the retry + // request needs no request-level tableName. + List recordsToRetry = response.getRecordsToRetry(); + if (!recordsToRetry.isEmpty()) { + BulkInsertResponse retryResponse = skyflowClient.vault().bulkInsert( + BulkInsertRequest.builder() + .records(new ArrayList<>(recordsToRetry)) + .build()); + System.out.println("retry response:\t" + retryResponse); + } } catch (SkyflowException e) { - // Step 9: Handle any errors that occur during the process + // Step 11: Handle any errors that occur during the process System.err.println("Error in Skyflow operations: " + e.getMessage()); } } diff --git a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java index 4dc4d67c..b947239c 100644 --- a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java +++ b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java @@ -10,12 +10,15 @@ import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertOptions; +import com.skyflow.vault.data.BulkInsertResponseRecord; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.UpsertOptions; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -66,7 +69,7 @@ public static void main(String[] args) { } // Step 5: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" - // (default) or "REPLACE". + // or "REPLACE" — if omitted, the vault treats it the same as "UPDATE". List upsertColumns = new ArrayList<>(); upsertColumns.add(""); @@ -92,13 +95,36 @@ public static void main(String[] args) { // Step 8: Execute the async bulk insert operation and handle response using callbacks CompletableFuture future = skyflowClient.vault().bulkInsertAsync(request, options); - // Add success and error callbacks future.thenAccept(response -> { System.out.println("Async bulk insert resolved with response:\t" + response); + + // Read the summary, then walk the per-record outcomes. A record succeeded when + // its error is null; requestId identifies the batch an error came from and is + // set on failures only — the same header this sample attaches to the outgoing + // request shows up here on any batch that failed. + System.out.println("inserted:\t" + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + + for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] skyflowId=%s%n", record.getIndex(), record.getSkyflowId()); + // getTokens() returns a typed Map> + // - no casting needed to read token/tokenGroupName. + for (Map.Entry> column : record.getTokens().entrySet()) { + for (Token token : column.getValue()) { + System.out.printf(" %s[%s] -> %s%n", + column.getKey(), token.getTokenGroupName(), token.getToken()); + } + } + } else { + System.out.printf("[%d] failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getHttpCode(), record.getError(), record.getRequestId()); + } + } }).exceptionally(throwable -> { System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); throw new CompletionException(throwable); - }); + }).join(); // sample-run only: block so main() doesn't exit before the async callback prints } catch (Exception e) { // Step 9: Handle any synchronous errors that occur during setup System.err.println("Error in Skyflow operations:\t" + e.getMessage()); @@ -110,4 +136,4 @@ public static String getRequestId() { System.out.println("id=>" + id); return id; } -} +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/Skyflow.java b/flowvault/src/main/java/com/skyflow/Skyflow.java index 5c2e682c..c2647824 100644 --- a/flowvault/src/main/java/com/skyflow/Skyflow.java +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -36,8 +36,7 @@ public static SkyflowClientBuilder builder() { } public VaultConfig getVaultConfig() { - Object[] array = this.builder.vaultConfigMap.values().toArray(); - return (VaultConfig) array[0]; + return this.builder.vaultConfigMap.values().stream().findFirst().orElse(null); } /** diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index d6011dec..31d1b331 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -49,6 +49,7 @@ import com.skyflow.vault.data.ErrorRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.UpsertOptions; @@ -351,7 +352,7 @@ public static BulkInsertResponseRecord createInsertErrorRecord(Map handleBulkInsertBatchException( } else { errorMessage = apiException.getMessage(); } - err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), errorMessage, requestId); } allRecords.add(err); @@ -432,7 +433,7 @@ public static List handleBulkInsertBatchException( if (allRecords.isEmpty()) { for (int j = 0; j < batch.size(); j++) { - allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); + allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); indexNumber++; } } @@ -452,7 +453,7 @@ public static List handleBulkInsertBatchException( if (message == null || message.isEmpty() || message.trim().isEmpty()){ message = ex.getMessage(); } - BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, message, null); + BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -760,7 +761,8 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo indexNumber, current.getTableName().orElse(null), current.getSkyflowId().orElse(null), - current.getTokens().orElse(null), + Token.parseTokens(current.getTokens().orElse(null)), + current.getData().orElse(null), current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), 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 1ea6b757..1d85c7b6 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; +import java.util.List; import java.util.Map; // Bulk counterpart of InsertResponseRecord. Adds the caller-facing position of the record @@ -10,10 +11,21 @@ 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)} + * instead, which also lets you populate {@code data}. This overload always leaves {@code data} null. + */ + @Deprecated(since = "1.0.2", forRemoval = true) public BulkInsertResponseRecord(int index, String tableName, String skyflowId, - Map fields, Map hashedData, + Map> tokens, Map hashedData, int httpCode, String error, String requestId) { - super(tableName, skyflowId, fields, hashedData, httpCode, error); + this(index, tableName, skyflowId, tokens, null, hashedData, httpCode, error, requestId); + } + + 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); this.index = index; this.requestId = 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 442d3749..57c7bcf9 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -1,20 +1,36 @@ package com.skyflow.vault.data; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.logger.LogUtil; + +import java.util.List; import java.util.Map; public class InsertResponseRecord { private final String tableName; private final String skyflowId; - private final Map fields; + private final Map> tokens; + private final Map data; private final Map hashedData; private final int httpCode; private final String error; - public InsertResponseRecord(String tableName, String skyflowId, Map fields, + /** + * @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); + } + + public InsertResponseRecord(String tableName, String skyflowId, Map> tokens, + Map data, Map hashedData, int httpCode, String error) { this.tableName = tableName; this.skyflowId = skyflowId; - this.fields = fields; + this.tokens = tokens; + this.data = data; this.hashedData = hashedData; this.httpCode = httpCode; this.error = error; @@ -28,8 +44,27 @@ public String getSkyflowId() { return skyflowId; } - public Map getFields() { - return fields; + /** + * 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; + } + + /** + * @deprecated Response key 'fields' is deprecated. Use {@link #getTokens()} instead. + */ + @Deprecated(since = "1.0.2", forRemoval = true) + public Map> getFields() { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_INSERT_FIELDS_GETTER.getLog()); + return getTokens(); + } + + public Map getData() { + return data; } public Map getHashedData() { diff --git a/flowvault/src/main/java/com/skyflow/vault/data/Token.java b/flowvault/src/main/java/com/skyflow/vault/data/Token.java new file mode 100644 index 00000000..e84efb8f --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/Token.java @@ -0,0 +1,103 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * One token-group outcome for a single column value, as returned inside + * {@link InsertResponseRecord#getTokens()}. A column tokenized against more than one + * token group comes back as a list of these, one per group. + */ +public class Token { + @Expose(serialize = true) + private final String token; + @Expose(serialize = true) + private final String tokenGroupName; + + public Token(String token, String tokenGroupName) { + this.token = token; + this.tokenGroupName = tokenGroupName; + } + + public String getToken() { + return token; + } + + public String getTokenGroupName() { + return tokenGroupName; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } + + /** + * Parses the API's raw, generically-typed per-column token data (as returned by the wire + * type, {@code Map}) into {@code Map>}. The API models + * a column's tokens generically to stay flexible, so this parses every shape that generic + * value is known to take — a list of {@code {token, tokenGroupName}} entries (a column + * tokenized against more than one group), a single such entry, or a bare token value with + * no group information — into a consistently-typed {@code List} per column. + * + *

Returns {@code null} when {@code rawTokens} is {@code null} (e.g. a failed record). A + * column whose raw value cannot be parsed into any of the above shapes is omitted, rather + * than throwing. A {@code null} element inside a column's list is skipped the same way. + */ + public static Map> parseTokens(Map rawTokens) { + if (rawTokens == null) { + return null; + } + Map> parsed = new LinkedHashMap<>(); + for (Map.Entry entry : rawTokens.entrySet()) { + List tokens = parseTokenEntries(entry.getValue()); + if (tokens != null) { + parsed.put(entry.getKey(), tokens); + } + } + return parsed; + } + + private static List parseTokenEntries(Object rawValue) { + if (rawValue == null) { + return null; + } + List parsed = new ArrayList<>(); + if (rawValue instanceof List) { + for (Object entry : (List) rawValue) { + Token token = toToken(entry); + if (token != null) { + parsed.add(token); + } + } + } else { + Token token = toToken(rawValue); + if (token != null) { + parsed.add(token); + } + } + return parsed; + } + + private static Token toToken(Object entry) { + if (entry instanceof Map) { + Map entryMap = (Map) entry; + Object token = entryMap.get("token"); + Object tokenGroupName = entryMap.get("tokenGroupName"); + return new Token(token != null ? token.toString() : null, + tokenGroupName != null ? tokenGroupName.toString() : null); + } + if (entry != null) { + // A column tokenized against a single, unnamed group can come back as a bare value. + return new Token(entry.toString(), null); + } + return null; + } +} diff --git a/flowvault/src/test/java/com/skyflow/SkyflowTests.java b/flowvault/src/test/java/com/skyflow/SkyflowTests.java index 7986e68a..4052c4a9 100644 --- a/flowvault/src/test/java/com/skyflow/SkyflowTests.java +++ b/flowvault/src/test/java/com/skyflow/SkyflowTests.java @@ -509,11 +509,66 @@ public void testVaultById_removedVaultThrowsWhileOthersStillResolve() throws Sky } } - // ── getVaultConfig ──────────────────────────────────────────────────────── + // ── getVaultConfig() ────────────────────────────────────────────────────── + + @Test + public void testGetVaultConfig_returnsTheOnlyConfiguredVault() throws SkyflowException { + // addVaultConfigTemplate stores cloneVaultConfig(vaultConfig), not the same reference, + // so compare fields rather than identity against the config passed into addVaultConfig. + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertEquals("vault1", client.getVaultConfig().getVaultId()); + Assert.assertEquals("cluster1", client.getVaultConfig().getClusterId()); + } + + @Test + public void testGetVaultConfig_returnsFirstConfiguredVaultAmongSeveral() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + + Assert.assertEquals("vault1", client.getVaultConfig().getVaultId()); + // Consistent with vault(): the no-arg accessor always resolves to the first-registered vault. + Assert.assertSame(client.getVaultConfig("vault1"), client.getVaultConfig()); + } + + @Test + public void testGetVaultConfig_returnsNullWhenNoConfigExists() { + // Regression test: this used to be Object[] array = ...toArray(); return (VaultConfig) + // array[0], which threw ArrayIndexOutOfBoundsException on an empty map instead of + // failing gracefully like every other lookup in this class. + Assert.assertNull(Skyflow.builder().build().getVaultConfig()); + } + + @Test + public void testGetVaultConfig_returnsNullAfterTheOnlyVaultIsRemoved() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + client.removeVaultConfig("vault1"); + + Assert.assertNull(client.getVaultConfig()); + } + + @Test + public void testGetVaultConfig_fallsBackToTheRemainingVaultAfterTheFirstIsRemoved() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + client.removeVaultConfig("vault1"); + + Assert.assertEquals("vault2", client.getVaultConfig().getVaultId()); + } + + // ── getVaultConfig(vaultId) ─────────────────────────────────────────────── @Test public void testGetVaultConfig_returnsNullForUnknownVaultId() throws SkyflowException { Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); Assert.assertNull(client.getVaultConfig("vault-unknown")); } + + @Test + public void testGetVaultConfig_byIdReturnsNullWhenNoConfigExists() { + Assert.assertNull(Skyflow.builder().build().getVaultConfig("vault1")); + } } diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index 4e034bfd..2ba3effd 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -1687,9 +1687,12 @@ public void testHandleBulkTokenizeBatchException_nullBatchReturnsEmpty() { public void testFormatBulkInsertResponse_success() { Map tokens = new HashMap<>(); tokens.put("name", "tok-abc"); + Map data = new HashMap<>(); + data.put("name", "john"); V1RecordResponseObject record = V1RecordResponseObject.builder() .skyflowId("sky-id-1") .tokens(tokens) + .data(data) .build(); V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); @@ -1698,7 +1701,12 @@ public void testFormatBulkInsertResponse_success() { Assert.assertEquals(1, result.getRecords().size()); BulkInsertResponseRecord inserted = result.getRecords().get(0); Assert.assertEquals("sky-id-1", inserted.getSkyflowId()); - Assert.assertEquals(tokens, inserted.getFields()); + // The wire type's raw tokens map is parsed into typed Token objects before reaching the + // caller - see ResponseComponentTests's Token.parseTokens() tests for the parsing logic. + Assert.assertEquals("tok-abc", inserted.getTokens().get("name").get(0).getToken()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals("tok-abc", inserted.getFields().get("name").get(0).getToken()); + Assert.assertEquals(data, inserted.getData()); Assert.assertEquals(0, inserted.getIndex()); Assert.assertEquals(200, inserted.getHttpCode()); Assert.assertNull(inserted.getError()); diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 72918e46..7d668283 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -41,6 +41,7 @@ import com.skyflow.vault.data.DeleteTokensOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestInterceptor; +import com.skyflow.vault.data.Token; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.TokenizeOptions; import com.skyflow.vault.data.TokenizeRequestRecord; @@ -704,14 +705,14 @@ public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { Assert.assertEquals(1, response.getRecords().size()); BulkInsertResponseRecord inserted = response.getRecords().get(0); - Assert.assertNotNull(inserted.getFields()); - // The token map is surfaced verbatim as `fields`, so a List token shape survives intact. - Object field1Tokens = inserted.getFields().get("field1"); - Assert.assertTrue(field1Tokens instanceof List); - Assert.assertEquals(1, ((List) field1Tokens).size()); - Map field1Token = (Map) ((List) field1Tokens).get(0); - Assert.assertEquals("tok-xyz", field1Token.get("token")); - Assert.assertEquals("group1", field1Token.get("tokenGroupName")); + Assert.assertNotNull(inserted.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(inserted.getTokens(), inserted.getFields()); + // The wire type's List token shape is parsed into typed Token objects - no casting. + List field1Tokens = inserted.getTokens().get("field1"); + Assert.assertEquals(1, field1Tokens.size()); + Assert.assertEquals("tok-xyz", field1Tokens.get(0).getToken()); + Assert.assertEquals("group1", field1Tokens.get(0).getTokenGroupName()); } // Tests for the unary query / get controller methods were removed: VaultController is bulk-only now. diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java index fc6b4e65..8b58b873 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -40,8 +40,8 @@ public void testBulkInsertResponse_oneArgConstructorLeavesSummaryAndRetryDepende @Test public void testBulkInsertResponse_twoArgConstructorComputesSummary() { List records = Arrays.asList( - new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null), - new BulkInsertResponseRecord(1, null, null, null, null, 400, "failed", null)); + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, null, 200, null, null), + new BulkInsertResponseRecord(1, null, null, null, null, null, 400, "failed", null)); List originalPayload = new ArrayList<>(Arrays.asList( BulkInsertRequestRecord.builder().tableName("table1").build(), BulkInsertRequestRecord.builder().tableName("table1").build())); @@ -56,13 +56,15 @@ public void testBulkInsertResponse_twoArgConstructorComputesSummary() { @Test public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { - Map fields = new HashMap<>(); - fields.put("name", "token-name"); + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("token-name", "group1"))); + Map data = new HashMap<>(); + data.put("name", "john"); Map hashedData = new HashMap<>(); hashedData.put("name", "hashed-name"); BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 7, "table1", "id-1", fields, hashedData, 200, null, null); + 7, "table1", "id-1", tokens, data, hashedData, 200, null, null); BulkInsertResponse response = new BulkInsertResponse(Collections.singletonList(record)); @@ -70,7 +72,10 @@ public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { Assert.assertEquals(7, actual.getIndex()); Assert.assertEquals("table1", actual.getTableName()); Assert.assertEquals("id-1", actual.getSkyflowId()); - Assert.assertEquals(fields, actual.getFields()); + Assert.assertEquals(tokens, actual.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(tokens, actual.getFields()); + Assert.assertEquals(data, actual.getData()); Assert.assertEquals(hashedData, actual.getHashedData()); Assert.assertEquals(200, actual.getHttpCode()); Assert.assertNull(actual.getError()); @@ -86,10 +91,10 @@ public void testBulkInsertResponse_getRecordsToRetryFiltersRetryableStatusCodesO Arrays.asList(record0, record1, record2, record3)); List records = Arrays.asList( - new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null), // retryable (lower bound) - new BulkInsertResponseRecord(1, null, null, null, null, 400, "bad request", null), // not retryable - new BulkInsertResponseRecord(2, null, null, null, null, 599, "server error", null), // retryable (upper bound) - new BulkInsertResponseRecord(3, null, null, null, null, 529, "special case", null)); // explicitly excluded + new BulkInsertResponseRecord(0, null, null, null, null, null, 500, "server error", null), // retryable (lower bound) + new BulkInsertResponseRecord(1, null, null, null, null, null, 400, "bad request", null), // not retryable + new BulkInsertResponseRecord(2, null, null, null, null, null, 599, "server error", null), // retryable (upper bound) + new BulkInsertResponseRecord(3, null, null, null, null, null, 529, "special case", null)); // explicitly excluded BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); @@ -111,7 +116,7 @@ public void testBulkInsertResponse_toStringNotNull() { @Test public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInternals() { List records = Collections.singletonList( - new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null)); + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, null, 200, null, null)); List originalPayload = new ArrayList( Collections.singletonList(BulkInsertRequestRecord.builder().tableName("table1").build())); @@ -133,7 +138,7 @@ public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInte public void testBulkInsertResponse_getRecordsToRetryOnPerBatchResponseDoesNotThrow() { // The 1-arg constructor leaves originalPayload null. A 5xx record must not NPE here. List records = Collections.singletonList( - new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null)); + new BulkInsertResponseRecord(0, null, null, null, null, null, 500, "server error", null)); BulkInsertResponse response = new BulkInsertResponse(records); 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 273c4c85..b3dfa849 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -3,6 +3,7 @@ import org.junit.Assert; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -10,34 +11,86 @@ /** * Tests for the response/success/summary building-block classes that carry real - * constructor logic or toString() serialization: {@link Success}, {@link Summary}, - * {@link Token}, {@link TokenizeResponseToken}, {@link TokenizeResponseRecord}, - * {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, + * constructor logic or toString() serialization: {@link Token}, {@link TokenizeResponseToken}, + * {@link TokenizeResponseRecord}, {@link BulkTokenizeResponseRecord}, {@link TokenizeSummary}, * {@link DeleteTokensRecord}, {@link BulkDeleteTokensResponseRecord}, * {@link DeleteTokensSummary}, {@link DetokenizeSummary}, * {@link ErrorRecord} and {@link DetokenizeResponseObject}. */ public class ResponseComponentTests { - // Tests for Success, Summary and Token were removed: the bulk insert response contract - // replaced those classes with BulkInsertResponseRecord / BulkSummary, covered below. + // 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. // ── BulkInsertResponseRecord ───────────────────────────────────────────── @Test public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { - Map fields = new HashMap<>(); - fields.put("name", "tok-1"); + 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"); BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 2, "persons", "skyflow-id-1", fields, hashedData, 200, null, null); + 2, "persons", "skyflow-id-1", tokens, data, hashedData, 200, null, null); Assert.assertEquals(2, record.getIndex()); Assert.assertEquals("persons", record.getTableName()); Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); - Assert.assertEquals(fields, record.getFields()); + Assert.assertEquals(tokens, record.getTokens()); + // getFields() is deprecated but still delegates to getTokens() for backward compatibility. + Assert.assertEquals(tokens, record.getFields()); + Assert.assertEquals(data, record.getData()); + 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()); + } + + @Test + @SuppressWarnings("deprecation") + public void testBulkInsertResponseRecord_deprecatedConstructorAndGetFieldsStillWork() { + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + // The pre-existing (data-less) constructor overload and getFields() are both deprecated, + // but must keep working unchanged for callers who haven't migrated yet. + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 2, "persons", "skyflow-id-1", tokens, hashedData, 200, null, null); + + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(tokens, record.getFields()); + Assert.assertNull(record.getData()); + Assert.assertEquals(hashedData, record.getHashedData()); + } + + @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. + Map> tokens = new HashMap<>(); + tokens.put("name", Collections.singletonList(new Token("tok-1", "group1"))); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + InsertResponseRecord record = new InsertResponseRecord( + "persons", "skyflow-id-1", tokens, hashedData, 200, null); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(tokens, record.getFields()); + Assert.assertNull(record.getData()); Assert.assertEquals(hashedData, record.getHashedData()); Assert.assertEquals(200, record.getHttpCode()); Assert.assertNull(record.getError()); @@ -46,26 +99,159 @@ public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { @Test public void testBulkInsertResponseRecord_errorCase() { BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 3, null, null, null, null, 500, "Internal Server Error", null); + 3, null, null, null, null, null, 500, "Internal Server Error", null); Assert.assertEquals(3, record.getIndex()); Assert.assertEquals(500, record.getHttpCode()); Assert.assertEquals("Internal Server Error", record.getError()); Assert.assertNull(record.getTableName()); Assert.assertNull(record.getSkyflowId()); + Assert.assertNull(record.getTokens()); Assert.assertNull(record.getFields()); + Assert.assertNull(record.getData()); Assert.assertNull(record.getHashedData()); } @Test public void testBulkInsertResponseRecord_toStringSerializesNulls() { BulkInsertResponseRecord record = new BulkInsertResponseRecord( - 0, "persons", "skyflow-id-2", null, null, 200, null, null); + 0, "persons", "skyflow-id-2", null, null, null, 200, null, null); String json = record.toString(); Assert.assertNotNull(json); Assert.assertTrue(json.contains("skyflow-id-2")); Assert.assertTrue(json.contains("\"index\":0")); - Assert.assertTrue(json.contains("\"fields\":null")); + Assert.assertTrue(json.contains("\"tokens\":null")); + Assert.assertTrue(json.contains("\"data\":null")); + } + + // ── Token / Token.parseTokens() ─────────────────────────────────────────── + + @Test + public void testToken_gettersReturnConstructorValues() { + Token token = new Token("tok-1", "group1"); + Assert.assertEquals("tok-1", token.getToken()); + Assert.assertEquals("group1", token.getTokenGroupName()); + } + + @Test + public void testToken_toStringSerializesFields() { + Token token = new Token("tok-1", "group1"); + String json = token.toString(); + Assert.assertTrue(json.contains("tok-1")); + Assert.assertTrue(json.contains("group1")); + } + + @Test + public void testParseTokens_returnsNullWhenRawTokensIsNull() { + Assert.assertNull(Token.parseTokens(null)); + } + + @Test + public void testParseTokens_parsesAListOfTokenGroupEntriesPerColumn() { + // The real, tested API shape for a column tokenized against more than one group - + // see VaultControllerTests.testBulkInsert_successWithListOfMapsTokenShape. + Map entry1 = new HashMap<>(); + entry1.put("token", "tok-a"); + entry1.put("tokenGroupName", "tg1"); + Map entry2 = new HashMap<>(); + entry2.put("token", "tok-b"); + entry2.put("tokenGroupName", "tg2"); + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Arrays.asList(entry1, entry2)); + + List col1 = Token.parseTokens(rawTokens).get("col1"); + Assert.assertEquals(2, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); + Assert.assertEquals("tok-b", col1.get(1).getToken()); + Assert.assertEquals("tg2", col1.get(1).getTokenGroupName()); + } + + @Test + public void testParseTokens_parsesASingleTokenGroupEntryNotWrappedInAList() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); + + List col1 = Token.parseTokens(rawTokens).get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertEquals("tg1", col1.get(0).getTokenGroupName()); + } + + @Test + public void testParseTokens_parsesABareTokenValueWithNoGroupInfo() { + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", "tok-abc"); + + List col1 = Token.parseTokens(rawTokens).get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-abc", col1.get(0).getToken()); + Assert.assertNull(col1.get(0).getTokenGroupName()); + } + + @Test + public void testParseTokens_handlesMultipleColumnsIndependently() { + Map groupedEntry = new HashMap<>(); + groupedEntry.put("token", "tok-a"); + groupedEntry.put("tokenGroupName", "tg1"); + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Collections.singletonList(groupedEntry)); + rawTokens.put("col2", "tok-bare"); + + Map> parsed = Token.parseTokens(rawTokens); + Assert.assertEquals("tg1", parsed.get("col1").get(0).getTokenGroupName()); + Assert.assertEquals("tok-bare", parsed.get("col2").get(0).getToken()); + Assert.assertNull(parsed.get("col2").get(0).getTokenGroupName()); + } + + @Test + public void testParseTokens_omitsAColumnWithANullValue() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); + rawTokens.put("col2", null); + + Map> parsed = Token.parseTokens(rawTokens); + Assert.assertTrue(parsed.containsKey("col1")); + Assert.assertFalse(parsed.containsKey("col2")); + } + + @Test + public void testParseTokens_mapEntryMissingTokenGroupNameKeyParsesAsNull() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + // no "tokenGroupName" key at all - distinct from the bare-value case, since here the + // raw entry is still a Map, just missing one of the two expected keys. + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", entry); + + List col1 = Token.parseTokens(rawTokens).get("col1"); + Assert.assertEquals("tok-a", col1.get(0).getToken()); + Assert.assertNull(col1.get(0).getTokenGroupName()); + } + + @Test + public void testParseTokens_skipsNullEntriesWithinAList() { + Map entry = new HashMap<>(); + entry.put("token", "tok-a"); + entry.put("tokenGroupName", "tg1"); + + Map rawTokens = new HashMap<>(); + rawTokens.put("col1", Arrays.asList(entry, null)); + + List col1 = Token.parseTokens(rawTokens).get("col1"); + Assert.assertEquals(1, col1.size()); + Assert.assertEquals("tok-a", col1.get(0).getToken()); } // ── BulkSummary ──────────────────────────────────────────────────────────