diff --git a/.cspell.json b/.cspell.json index 57a9d3af..8b9af8e4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -97,11 +97,18 @@ "nocreds", "nodir", "detok", - "qhdmceurtnlz", "ngrok", "obac", "siom", - "recordss" + "vaultid", + "recordss", + "synthesise", + "synthesised", + "deserialise", + "deserialised", + "unmodelled", + "recordss", + "rarr" ], "languageSettings": [ { @@ -121,6 +128,7 @@ "**/target/**", "*.lock", "Rule/**", + "**/src/main/java/com/skyflow/generated/**", "**/generated/**", "**/*.ts", "**/processed-*", diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index f50424b6..bdc19096 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -29,23 +29,23 @@ jobs: cache: 'maven' - name: Verify API surface snapshot - run: mvn -B install -pl common,v2 -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true + run: mvn -B install -pl common,skyvault -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true - name: Show API surface diff if: failure() run: | echo "### API surface changes detected ###" - echo "See v2/target/japicmp/default-cli.diff for the full comparison against v2/api-report/skyflow-java.baseline.jar." + echo "See skyvault/target/japicmp/default-cli.diff for the full comparison against skyvault/api-report/skyflow-java.baseline.jar." echo "If this change is intentional, run scripts/contract-snapshot-update.sh and commit the updated baseline jar." echo "" - cat v2/target/japicmp/default-cli.diff || true + cat skyvault/target/japicmp/default-cli.diff || true - name: Upload API surface diff on failure if: failure() uses: actions/upload-artifact@v4 with: name: api-surface-diff - path: v2/target/japicmp/** + path: skyvault/target/japicmp/** retention-days: 7 # The step above only shows a diff when the CURRENT build differs from the @@ -61,7 +61,7 @@ jobs: if: always() && github.event.pull_request run: | git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 - if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}" HEAD -- v2/api-report/skyflow-java.baseline.jar | grep -q .; then + if git diff --name-only "origin/${{ github.event.pull_request.base.ref }}" HEAD -- skyvault/api-report/skyflow-java.baseline.jar | grep -q .; then echo "changed=true" >> "$GITHUB_OUTPUT" else echo "changed=false" >> "$GITHUB_OUTPUT" @@ -71,16 +71,16 @@ jobs: if: always() && steps.baseline-diff-check.outputs.changed == 'true' run: | curl -sL -o /tmp/japicmp-cli.jar "https://repo.maven.apache.org/maven2/com/github/siom79/japicmp/japicmp/0.26.0/japicmp-0.26.0-jar-with-dependencies.jar" - mvn -q -B dependency:build-classpath -pl v2 -Dmdep.outputFile=/tmp/v2-classpath.txt -Dmaven.javadoc.skip=true -Dgpg.skip=true - git show "origin/${{ github.event.pull_request.base.ref }}:v2/api-report/skyflow-java.baseline.jar" > /tmp/old-baseline.jar + mvn -q -B dependency:build-classpath -pl skyvault -Dmdep.outputFile=/tmp/skyvault-classpath.txt -Dmaven.javadoc.skip=true -Dgpg.skip=true + git show "origin/${{ github.event.pull_request.base.ref }}:skyvault/api-report/skyflow-java.baseline.jar" > /tmp/old-baseline.jar java -jar /tmp/japicmp-cli.jar \ -o /tmp/old-baseline.jar \ - -n v2/api-report/skyflow-java.baseline.jar \ + -n skyvault/api-report/skyflow-java.baseline.jar \ -a protected \ -e "com.skyflow.generated.*;com.skyflow.utils.*" \ - --old-classpath "$(cat /tmp/v2-classpath.txt)" \ - --new-classpath "$(cat /tmp/v2-classpath.txt)" \ + --old-classpath "$(cat /tmp/skyvault-classpath.txt)" \ + --new-classpath "$(cat /tmp/skyvault-classpath.txt)" \ -m \ --markdown > /tmp/contract-baseline-diff.md || true @@ -94,7 +94,7 @@ jobs: const fs = require('fs'); const summary = fs.readFileSync('/tmp/contract-baseline-diff.md', 'utf8'); const marker = ''; - const body = `${marker}\n## Contract baseline change detected\n\nThis PR updates \`v2/api-report/skyflow-java.baseline.jar\` (the approved public API contract). Here is exactly what it changes, comparing the baseline on \`${{ github.event.pull_request.base.ref }}\` against the baseline committed in this PR:\n\n${summary}`; + const body = `${marker}\n## Contract baseline change detected\n\nThis PR updates \`skyvault/api-report/skyflow-java.baseline.jar\` (the approved public API contract). Here is exactly what it changes, comparing the baseline on \`${{ github.event.pull_request.base.ref }}\` against the baseline committed in this PR:\n\n${summary}`; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/endorlabsScan.yml b/.github/workflows/endorlabsScan.yml index 927009fe..762b742e 100644 --- a/.github/workflows/endorlabsScan.yml +++ b/.github/workflows/endorlabsScan.yml @@ -40,7 +40,7 @@ jobs: run: | # Surefire runs each module's tests with the module directory as the working # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. - for module in common v2 flowvault; do + for module in common skyvault flowvault; do cp .env "$module/.env" cp credentials.json "$module/credentials.json" done diff --git a/.github/workflows/internal-release.yml b/.github/workflows/internal-release.yml index a246a6e3..0fea51db 100644 --- a/.github/workflows/internal-release.yml +++ b/.github/workflows/internal-release.yml @@ -21,7 +21,7 @@ jobs: if [[ "${{ github.ref_name }}" == flowvault-release/* ]]; then echo "module=flowvault" >> "$GITHUB_OUTPUT" else - echo "module=v2" >> "$GITHUB_OUTPUT" + echo "module=skyvault" >> "$GITHUB_OUTPUT" fi build-and-deploy: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7360ea3..5489eeea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,7 @@ jobs: run: | # Surefire runs each module's tests with the module directory as the working # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. - for module in common v2 flowvault; do + for module in common skyvault flowvault; do cp .env "$module/.env" cp credentials.json "$module/credentials.json" done @@ -42,10 +42,53 @@ jobs: - name: Build & Run tests with Maven run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true - - name: Codecov - uses: codecov/codecov-action@v2.1.0 + # JaCoCo records packages as "com/skyflow/..." with no module prefix, and all three + # modules share the com.skyflow package (deliberate split-package convention). So + # "com/skyflow/config/VaultConfig.java" matches BOTH skyvault and flowvault, Codecov + # resolves it to one of them, and the other module's file silently reports no data. + # Prefixing each report with its own source root makes every path unique and match the + # repo exactly, so all three modules' files are attributed correctly. + - name: Qualify JaCoCo report paths with their module + run: | + for module in common skyvault flowvault; do + report="$module/target/site/jacoco/jacoco.xml" + if [ ! -f "$report" ]; then + echo "Error: expected $report to exist after the build." + exit 1 + fi + tmp="$(mktemp)" + sed "s| "$tmp" + mv "$tmp" "$report" + done + + # One upload per module, each with its own flag, so Codecov reports per-module + # coverage instead of merging three same-named package trees into one number. + - name: Codecov (common) + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: common/target/site/jacoco/jacoco.xml + flags: common + name: codecov-common + verbose: true + fail_ci_if_error: true + + - name: Codecov (skyvault) + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: skyvault/target/site/jacoco/jacoco.xml + flags: skyvault + name: codecov-skyvault + verbose: true + fail_ci_if_error: true + + - name: Codecov (flowvault) + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: target/site/jacoco/jacoco.xml - name: codecov-skyflow-java + files: flowvault/target/site/jacoco/jacoco.xml + flags: flowvault + name: codecov-flowvault verbose: true + fail_ci_if_error: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0002d3eb..822b1288 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -43,7 +43,7 @@ jobs: run: | # Surefire runs each module's tests with the module directory as the working # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. - for module in common v2 flowvault; do + for module in common skyvault flowvault; do cp .env "$module/.env" cp credentials.json "$module/credentials.json" done @@ -51,17 +51,56 @@ jobs: - name: Build & Run tests with Maven run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true - # JaCoCo writes one report per module. The root pom is pom - # with no sources, so target/site/jacoco/jacoco.xml - correct when this was a - # single-module build - is never generated post common/v2/flowvault split, and - # codecov was silently uploading nothing. - - name: Codecov - uses: codecov/codecov-action@v2.1.0 + # JaCoCo records packages as "com/skyflow/..." with no module prefix, and all three + # modules share the com.skyflow package (deliberate split-package convention). So + # "com/skyflow/config/VaultConfig.java" matches BOTH skyvault and flowvault, Codecov + # resolves it to one of them, and the other module's file silently reports no data. + # Prefixing each report with its own source root makes every path unique and match the + # repo exactly, so all three modules' files are attributed correctly. + - name: Qualify JaCoCo report paths with their module + run: | + for module in common skyvault flowvault; do + report="$module/target/site/jacoco/jacoco.xml" + if [ ! -f "$report" ]; then + echo "Error: expected $report to exist after the build." + exit 1 + fi + tmp="$(mktemp)" + sed "s| "$tmp" + mv "$tmp" "$report" + done + + # One upload per module, each with its own flag, so Codecov reports per-module + # coverage instead of merging three same-named package trees into one number. + - name: Codecov (common) + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: common/target/site/jacoco/jacoco.xml + flags: common + name: codecov-common + verbose: true + fail_ci_if_error: true + + - name: Codecov (skyvault) + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: skyvault/target/site/jacoco/jacoco.xml + flags: skyvault + name: codecov-skyvault + verbose: true + fail_ci_if_error: true + + - name: Codecov (flowvault) + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: common/target/site/jacoco/jacoco.xml,v2/target/site/jacoco/jacoco.xml,flowvault/target/site/jacoco/jacoco.xml - name: codecov-skyflow-java + files: flowvault/target/site/jacoco/jacoco.xml + flags: flowvault + name: codecov-flowvault verbose: true + fail_ci_if_error: true spellcheck: name: Run spellcheck diff --git a/.github/workflows/shared-build-and-deploy.yml b/.github/workflows/shared-build-and-deploy.yml index 5cdfca73..842dbc58 100644 --- a/.github/workflows/shared-build-and-deploy.yml +++ b/.github/workflows/shared-build-and-deploy.yml @@ -135,7 +135,7 @@ jobs: run: | # Surefire runs each module's tests with the module directory as the working # directory, so dotenv and ./credentials.json lookups miss the repo-root copies. - for module in common v2 flowvault; do + for module in common skyvault flowvault; do cp .env "$module/.env" cp credentials.json "$module/credentials.json" done diff --git a/.gitignore b/.gitignore index 0efc6bd4..4f10424a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ target RUNNING_SAMPLES.md docs/superpowers/ + +# local sample configuration - may hold credentials +.env diff --git a/codecov.yml b/codecov.yml index 2a9a1108..0191c11b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,16 +1,90 @@ -comment: false +# Post a summary on every PR, broken down by flag (module) and component so all three of +# common / skyvault / flowvault are visible at a glance. require_changes: false keeps the +# comment on PRs that touch only one module, so the other two still report their coverage. +comment: + layout: "header, diff, flags, components, files, footer" + behavior: default + require_changes: false -# Component paths are prefixed with **/ (quoted - a bare leading * is a YAML -# alias) so they match every Maven module. Before the common/v2/flowvault -# split sources lived at src/main/java/...; they are now -# /src/main/java/..., so the unprefixed globs matched zero files and -# every component reported no coverage data. +# A project + patch status per module, so each of the three gets its own PR check rather +# than being averaged into one repo-wide number. informational keeps them advisory - they +# report the delta without blocking the merge. +coverage: + status: + project: + default: + target: auto + threshold: 1% + common: + flags: + - common + target: auto + threshold: 1% + informational: true + skyvault: + flags: + - skyvault + target: auto + threshold: 1% + informational: true + flowvault: + flags: + - flowvault + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + threshold: 1% + informational: true + +# One flag per Maven module. Each module is uploaded separately in CI so its coverage is +# reported on its own rather than merged into a single repo-wide number. carryforward keeps +# the last known coverage for a module when a run does not upload it (e.g. a partial build). +flags: + common: + paths: + - common/src/main/java/ + carryforward: true + skyvault: + paths: + - skyvault/src/main/java/ + carryforward: true + flowvault: + paths: + - flowvault/src/main/java/ + carryforward: true + +# Components give two independent breakdowns of the same coverage data: +# - one per module, so a drop can be traced to common / skyvault / flowvault +# - one per package, so a drop can be traced to controllers / data / utils / ... +# +# Package paths are prefixed with **/ (quoted - a bare leading * is a YAML alias) so they +# match every module. All three modules share the com.skyflow package name, so the JaCoCo +# reports are rewritten in CI to carry their module's source root; without that, a path like +# com/skyflow/config/VaultConfig.java is ambiguous and Codecov attributes it to a single +# module while the others report no data. See the "Qualify JaCoCo report paths" CI step. component_management: default_rules: statuses: - type: project target: auto individual_components: + # -- per module ------------------------------------------------------------ + - component_id: module_common + name: "Module: common" + paths: + - "common/src/main/java/**" + - component_id: module_skyvault + name: "Module: skyvault" + paths: + - "skyvault/src/main/java/**" + - component_id: module_flowvault + name: "Module: flowvault" + paths: + - "flowvault/src/main/java/**" + # -- per package, across all modules --------------------------------------- - component_id: service_account name: Service Account paths: @@ -55,3 +129,18 @@ component_management: name: Errors paths: - "**/src/main/java/com/skyflow/errors/**" + - component_id: enums + name: Enums + paths: + - "**/src/main/java/com/skyflow/enums/**" + - component_id: logs + name: Logs + paths: + - "**/src/main/java/com/skyflow/logs/**" + +# Generated REST/auth clients and sample code are not hand-written and are already excluded +# from the JaCoCo reports by the root pom; ignore them here too so they never skew a target. +ignore: + - "**/src/main/java/com/skyflow/generated/**" + - "**/samples/**" + - "**/src/test/**" diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java index ccad4b13..2f52addf 100644 --- a/common/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -154,6 +154,8 @@ public enum ErrorMessage { 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."), + 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."), // Connection InvalidRequestHeaders("%s0 Validation error. Request headers aren't valid. Specify valid request headers."), diff --git a/common/src/main/java/com/skyflow/logs/ErrorLogs.java b/common/src/main/java/com/skyflow/logs/ErrorLogs.java index b1f0109b..1a405bb5 100644 --- a/common/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/common/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -133,6 +133,8 @@ public enum ErrorLogs { 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."), + 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_REJECTED("Delete request resulted in failure."), 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."), diff --git a/flowvault/pom.xml b/flowvault/pom.xml index 74693016..9b34fa4d 100644 --- a/flowvault/pom.xml +++ b/flowvault/pom.xml @@ -11,7 +11,7 @@ skyflow-flowvault-java - 3.0.0-beta.13-dev.0d4db0a + 3.0.0-beta.13-dev.f72e2218 jar ${project.groupId}:${project.artifactId} Skyflow V3 SDK for the Java programming language diff --git a/samples/README.md b/flowvault/samples/README.md similarity index 100% rename from samples/README.md rename to flowvault/samples/README.md diff --git a/flowvault/samples/pom.xml b/flowvault/samples/pom.xml new file mode 100644 index 00000000..390c0251 --- /dev/null +++ b/flowvault/samples/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.example + skyflow-javasdk-sample + 1.0.0 + + + 8 + 8 + + + + + + com.skyflow + skyflow-flowvault-java + 3.0.0-beta.13-dev.0d4db0a + + + + + + central + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + false + + + + maven_central + Maven Central + https://repo.maven.apache.org/maven2/ + + + + + \ No newline at end of file diff --git a/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java diff --git a/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java diff --git a/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java diff --git a/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java diff --git a/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java diff --git a/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java similarity index 100% rename from samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java rename to flowvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java new file mode 100644 index 00000000..c6354fe8 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeAsync.java @@ -0,0 +1,82 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.TokenGroupRedactions; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This sample demonstrates how to perform an asynchronous bulk detokenize operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 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 + */ +public class BulkDetokenizeAsync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials using credentials string + String credentialsString = ""; + Credentials credentials = new Credentials(); + credentials.setCredentialsString(credentialsString); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare list of tokens to detokenize + List tokens = new ArrayList<>(); + tokens.add(""); + tokens.add(""); + + // Step 5: Configure token group redactions + TokenGroupRedactions tokenGroupRedaction = TokenGroupRedactions.builder() + .tokenGroupName("") + .redaction("") + .build(); + List tokenGroupRedactions = new ArrayList<>(); + tokenGroupRedactions.add(tokenGroupRedaction); + + // Step 6: Build the detokenize request + BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(tokenGroupRedactions) + .build(); + + // Step 7: Execute the async bulk detokenize operation and handle response using callbacks + CompletableFuture future = + skyflowClient.vault().bulkDetokenizeAsync(detokenizeRequest); + future.thenAccept(response -> { + System.out.println("Async bulk detokenize resolved with response:\t" + response); + }).exceptionally(throwable -> { + System.err.println("Async bulk detokenize rejected with error:\t" + throwable.getMessage()); + throw new CompletionException(throwable); + }); + } catch (SkyflowException e) { + // Step 8: Handle any synchronous errors that occur during setup + System.err.println("Error in Skyflow operations: " + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java new file mode 100644 index 00000000..957c23ff --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDetokenizeSync.java @@ -0,0 +1,74 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.TokenGroupRedactions; + +import java.util.ArrayList; +import java.util.List; + +/** + * This sample demonstrates how to perform a synchronous bulk detokenize operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 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 + */ +public class BulkDetokenizeSync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials using credentials string + String credentialsString = ""; + Credentials credentials = new Credentials(); + credentials.setCredentialsString(credentialsString); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare list of tokens to detokenize + List tokens = new ArrayList<>(); + tokens.add(""); + tokens.add(""); + + // Step 5: Configure token group redactions + TokenGroupRedactions tokenGroupRedaction = TokenGroupRedactions.builder() + .tokenGroupName("") + .redaction("") + .build(); + List tokenGroupRedactions = new ArrayList<>(); + tokenGroupRedactions.add(tokenGroupRedaction); + + // Step 6: Build the detokenize request + BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(tokenGroupRedactions) + .build(); + + // Step 7: Execute the bulk detokenize operation and print the response + BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); + System.out.println(detokenizeResponse); + } catch (SkyflowException e) { + // Step 8: 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/BulkInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java new file mode 100644 index 00000000..dff0deca --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertAsync.java @@ -0,0 +1,107 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.UpsertOptions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This sample demonstrates how to perform an asynchronous bulk insert operation using the Skyflow Java SDK. + * The process involves: + * 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 + */ +public class BulkInsertAsync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare first record for insertion + HashMap recordData1 = new HashMap<>(); + recordData1.put("", ""); + recordData1.put("", ""); + + BulkInsertRequestRecord insertRecord1 = BulkInsertRequestRecord + .builder() + .data(recordData1) + .build(); + + // Step 5: Prepare second record for insertion + HashMap recordData2 = new HashMap<>(); + recordData2.put("", ""); + recordData2.put("", ""); + + BulkInsertRequestRecord insertRecord2 = BulkInsertRequestRecord + .builder() + .data(recordData2) + .build(); + + // Step 6: Combine records into a Insert record list + List insertRecords = new ArrayList<>(); + insertRecords.add(insertRecord1); + insertRecords.add(insertRecord2); + + // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" + // (default) or "REPLACE". + List upsertColumns = new ArrayList<>(); + upsertColumns.add(""); + + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(upsertColumns) + .updateType("REPLACE") + .build(); + + // Step 8: Build the insert request with table name and insertRecords. + // tableName and upsert must sit at the same level — here, the request level. + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("") + .upsert(upsert) + .records(insertRecords) + .build(); + + // 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); + }).exceptionally(throwable -> { + System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); + throw new CompletionException(throwable); + }); + } catch (Exception e) { + // Step 10: Handle any synchronous errors that occur during setup + System.err.println("Error in Skyflow operations:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java new file mode 100644 index 00000000..c3ff5d60 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkInsertSync.java @@ -0,0 +1,100 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.UpsertOptions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. + * The process involves: + * 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 + */ +public class BulkInsertSync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare first record for insertion + HashMap recordData1 = new HashMap<>(); + recordData1.put("", ""); + recordData1.put("", ""); + + BulkInsertRequestRecord insertRecord1 = BulkInsertRequestRecord + .builder() + .data(recordData1) + .build(); + + // Step 5: Prepare second record for insertion + HashMap recordData2 = new HashMap<>(); + recordData2.put("", ""); + recordData2.put("", ""); + + BulkInsertRequestRecord insertRecord2 = BulkInsertRequestRecord + .builder() + .data(recordData2) + .build(); + + // Step 6: Combine records into a Insert record list + List insertRecords = new ArrayList<>(); + insertRecords.add(insertRecord1); + insertRecords.add(insertRecord2); + + // Step 7: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" + // (default) or "REPLACE". + List upsertColumns = new ArrayList<>(); + upsertColumns.add(""); + + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(upsertColumns) + .updateType("REPLACE") + .build(); + + // Step 8: Build the insert request with table name and insertRecords. + // tableName and upsert must sit at the same level — here, the request level. + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("") + .upsert(upsert) + .records(insertRecords) + .build(); + + // Step 9: Execute the bulk insert operation and print the response + BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); + System.out.println(response); + } catch (SkyflowException e) { + // Step 10: 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/BulkMultiTableInsertAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java new file mode 100644 index 00000000..c0f15834 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertAsync.java @@ -0,0 +1,109 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.UpsertOptions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This sample demonstrates how to perform an asynchronous bulk insert operation using the Skyflow Java SDK. + * The process involves: + * 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 + * + *

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. + */ +public class BulkMultiTableInsertAsync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare first record for insertion + HashMap recordData1 = new HashMap<>(); + recordData1.put("", ""); + recordData1.put("", ""); + + List upsertColumns = new ArrayList<>(); + upsertColumns.add(""); + + // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // Set .updateType("REPLACE") to replace the matched row instead. + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(upsertColumns) + .build(); + + BulkInsertRequestRecord insertRecord1 = BulkInsertRequestRecord + .builder() + .data(recordData1) + .tableName("") + .upsert(upsert) + .build(); + + // Step 5: Prepare second record for insertion + HashMap recordData2 = new HashMap<>(); + recordData2.put("", ""); + recordData2.put("", ""); + + BulkInsertRequestRecord insertRecord2 = BulkInsertRequestRecord + .builder() + .data(recordData2) + .tableName("") + .build(); + + // Step 6: Combine records into a Insert record list + List insertRecords = new ArrayList<>(); + insertRecords.add(insertRecord1); + insertRecords.add(insertRecord2); + + // Step 7: Build the insert request. No tableName here — each record carries its own. + BulkInsertRequest request = BulkInsertRequest.builder() + .records(insertRecords) + .build(); + + // 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); + }).exceptionally(throwable -> { + System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); + throw new CompletionException(throwable); + }); + } catch (Exception e) { + // Step 9: Handle any synchronous errors that occur during setup + System.err.println("Error in Skyflow operations:\t" + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java new file mode 100644 index 00000000..a3c1211b --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkMultiTableInsertSync.java @@ -0,0 +1,102 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.UpsertOptions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This sample demonstrates how to perform a synchronous bulk insert operation using the Skyflow Java SDK. + * The process involves: + * 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 + * + *

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. + */ +public class BulkMultiTableInsertSync { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with error logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare first record for insertion + HashMap recordData1 = new HashMap<>(); + recordData1.put("", ""); + recordData1.put("", ""); + + List upsertColumns = new ArrayList<>(); + upsertColumns.add(""); + + // upsert is optional; when updateType is omitted the vault defaults to "UPDATE". + // Set .updateType("REPLACE") to replace the matched row instead. + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(upsertColumns) + .build(); + + BulkInsertRequestRecord insertRecord1 = BulkInsertRequestRecord + .builder() + .data(recordData1) + .tableName("") + .upsert(upsert) + .build(); + + // Step 5: Prepare second record for insertion + HashMap recordData2 = new HashMap<>(); + recordData2.put("", ""); + recordData2.put("", ""); + + BulkInsertRequestRecord insertRecord2 = BulkInsertRequestRecord + .builder() + .data(recordData2) + .tableName("") + .build(); + + // Step 6: Combine records into a Insert record list + List insertRecords = new ArrayList<>(); + insertRecords.add(insertRecord1); + insertRecords.add(insertRecord2); + + // Step 7: Build the insert request. No tableName here — each record carries its own. + BulkInsertRequest request = BulkInsertRequest.builder() + .records(insertRecords) + .build(); + + // Step 8: Execute the bulk insert operation and print the response + BulkInsertResponse response = skyflowClient.vault().bulkInsert(request); + System.out.println(response); + } catch (SkyflowException e) { + // Step 9: 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 new file mode 100644 index 00000000..05941f8b --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java @@ -0,0 +1,113 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.InsertOptions; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.UpsertOptions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * This sample demonstrates how to attach custom headers to outgoing requests via a request + * interceptor on the options object. + * + *

Available keys on {@link CustomHeaderKey}: {@code SkyflowAccountId} ({@code x-skyflow-account-id}), + * {@code SkyflowAccountName} ({@code x-skyflow-account-name}) and {@code RequestIdHeader} + * ({@code x-request-id}). + * + *

The interceptor runs once per batch, so a per-request value such as a request id is generated + * fresh for each outgoing call rather than reused across the whole bulk operation. + */ +public class CustomHeaderExample { + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with a bearer token + Credentials credentials = new Credentials(); + credentials.setToken(""); + + // Step 2: Configure the vault with required parameters + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.DEV); + vaultConfig.setCredentials(credentials); + + // Step 3: Create Skyflow client instance with debug logging + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.DEBUG) + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Prepare the records for insertion + List insertRecords = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + HashMap recordData = new HashMap<>(); + recordData.put("", ""); + + BulkInsertRequestRecord insertRecord = BulkInsertRequestRecord + .builder() + .data(recordData) + .build(); + + insertRecords.add(insertRecord); + } + + // Step 5: Configure upsert. uniqueColumns is required; updateType accepts "UPDATE" + // (default) or "REPLACE". + List upsertColumns = new ArrayList<>(); + upsertColumns.add(""); + + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(upsertColumns) + .updateType("REPLACE") + .build(); + + // Step 6: Build the insert request. tableName and upsert both sit at the request level. + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("") + .upsert(upsert) + .records(insertRecords) + .build(); + + // Step 7: Attach a custom header through the interceptor + InsertOptions options = InsertOptions.builder() + .interceptor(ctx -> { + ctx.addHeader(CustomHeaderKey.RequestIdHeader, getRequestId()); // pass the request id here + }) + .build(); + + // 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); + }).exceptionally(throwable -> { + System.err.println("Async bulk insert rejected with error:\t" + throwable.getMessage()); + throw new CompletionException(throwable); + }); + } catch (Exception e) { + // Step 9: Handle any synchronous errors that occur during setup + System.err.println("Error in Skyflow operations:\t" + e.getMessage()); + } + } + + public static String getRequestId() { + String id = UUID.randomUUID().toString(); + System.out.println("id=>" + id); + return id; + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java b/flowvault/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java new file mode 100644 index 00000000..63fcd01b --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java @@ -0,0 +1,88 @@ +package com.example.vault; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; + +/** + * This sample demonstrates how to configure HTTP timeout and retry behavior in the Skyflow Java SDK. + * + *

Configurable settings (all optional): + *

    + *
  • {@code timeout} – overall call timeout in seconds (bounds the whole + * request including retries and backoff). Default: 60.
  • + *
  • {@code connectTimeout} – per-attempt connection-establishment timeout in seconds. + * Default: 10 (the underlying HTTP client default).
  • + *
  • {@code readTimeout} – per-attempt response-read timeout in seconds. + * Default: 10.
  • + *
  • {@code writeTimeout} – per-attempt request-write timeout in seconds. + * Default: 10.
  • + *
  • {@code maxRetries} – retry attempts after the first failure (retries on HTTP + * 408 / 429 / 5xx). Default: 0 — retries are OFF unless you set this (avoids auto-retrying + * non-idempotent writes).
  • + *
+ * + *

Backoff is not configurable. When {@code maxRetries} is greater than 0 the SDK backs off + * exponentially with jitter between attempts; there is no builder or {@code VaultConfig} setter to + * tune the delay. + * + *

How they relate: {@code timeout} is the total ceiling for the whole call (all + * attempts + backoff). {@code connectTimeout}/{@code readTimeout}/{@code writeTimeout} each bound a + * single phase of one attempt; because they are per attempt, their sum across retries can + * exceed {@code timeout}, but {@code timeout} always wins and cuts the call off. + * + *

Two levels + precedence: set client-wide defaults on {@code Skyflow.builder()}, and/or + * per-vault overrides on {@code VaultConfig}. The most specific value wins, resolved per field: + * per-vault → client-wide → SDK default. + */ +public class TimeoutAndRetryConfigExample { + + public static void main(String[] args) { + try { + // Step 1: Initialize credentials with the path to your service account key file + String filePath = ""; + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + + // Step 2: Configure the vault. Optionally override timeout/retry settings for THIS vault only. + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); + vaultConfig.setCredentials(credentials); + // Per-vault overrides (optional). Any field left unset inherits the client-wide default below, + // and then the SDK default. + vaultConfig.setTimeout(30); // seconds – tighter overall ceiling for this vault + vaultConfig.setConnectTimeout(5); // seconds – fail fast if the connection stalls + vaultConfig.setReadTimeout(20); // seconds – allow a slower response read + vaultConfig.setWriteTimeout(5); // seconds – bound the request write + vaultConfig.setMaxRetries(2); // fewer retries for this vault + + // Step 3: Create the Skyflow client. Client-wide defaults apply to every vault + // unless that vault overrides them (as above). + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) + .timeout(60) // seconds – client-wide overall call timeout + .connectTimeout(10) // seconds – client-wide per-attempt connect timeout + .readTimeout(15) // seconds – client-wide per-attempt read timeout + .writeTimeout(10) // seconds – client-wide per-attempt write timeout + .maxRetries(3) // client-wide retry attempts + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Use the client as usual. Requests now fail fast at the configured timeout and + // retry transient 408/429/5xx responses with exponential backoff + jitter. + System.out.println("Skyflow client configured with custom timeout & retry settings: " + skyflowClient); + + // Example (uncomment and fill in a real request to try it): + // BulkDetokenizeResponse response = skyflowClient.vault().bulkDetokenize(detokenizeRequest); + // System.out.println(response); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the process + System.err.println("Error in Skyflow operations: " + e.getMessage()); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/Skyflow.java b/flowvault/src/main/java/com/skyflow/Skyflow.java index ef62ce66..64551cb9 100644 --- a/flowvault/src/main/java/com/skyflow/Skyflow.java +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -55,6 +55,8 @@ public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder< private Integer readTimeout; private Integer writeTimeout; private Integer maxRetries; + private Long initialRetryDelayMillis; + private Long maxRetryDelayMillis; @Override protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { @@ -65,7 +67,8 @@ protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowExcept protected void onVaultConfigAdded(VaultConfig vaultConfig) throws SkyflowException { VaultController controller = new VaultController(vaultConfig, this.skyflowCredentials); controller.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, - this.writeTimeout, this.maxRetries); + this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis, + this.maxRetryDelayMillis); this.vaultClientsMap.put(vaultConfig.getVaultId(), controller); LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); } @@ -82,7 +85,8 @@ protected void onVaultConfigUpdated(VaultConfig updatedConfig) throws SkyflowExc updated.setVaultConfig(updatedConfig); } updated.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, - this.writeTimeout, this.maxRetries); + this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis, + this.maxRetryDelayMillis); } @Override @@ -142,6 +146,12 @@ private void carryVaultOverrides(VaultConfig incoming) throws SkyflowException { if (incoming.getMaxRetries() != null) { merged.setMaxRetries(incoming.getMaxRetries()); } + if (incoming.getInitialRetryDelayMillis() != null) { + merged.setInitialRetryDelayMillis(incoming.getInitialRetryDelayMillis()); + } + if (incoming.getMaxRetryDelayMillis() != null) { + merged.setMaxRetryDelayMillis(incoming.getMaxRetryDelayMillis()); + } // The HTTP settings above are resolved lazily on the next request, but the URL is // resolved once in the VaultClient constructor — which already ran with the old value. if (incoming.getVaultUrl() != null) { @@ -232,11 +242,38 @@ public SkyflowClientBuilder maxRetries(int maxRetries) { return this; } + /** + * Backoff before the first retry, in milliseconds. Default 500. Only applies when + * {@code maxRetries} is greater than zero. + *

+ * Precedence: a vault that sets {@link VaultConfig#setInitialRetryDelayMillis(Long)} + * wins; this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) { + this.initialRetryDelayMillis = initialRetryDelayMillis; + propagateHttpConfig(); + return this; + } + + /** + * Ceiling the exponential backoff grows to, in milliseconds. Default 2000. Only applies + * when {@code maxRetries} is greater than zero. + *

+ * Precedence: a vault that sets {@link VaultConfig#setMaxRetryDelayMillis(Long)} + * wins; this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { + this.maxRetryDelayMillis = maxRetryDelayMillis; + propagateHttpConfig(); + return this; + } + /** Push the current client-wide HTTP settings onto every vault controller built so far. */ private void propagateHttpConfig() { for (VaultController vault : this.vaultClientsMap.values()) { vault.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, - this.writeTimeout, this.maxRetries); + this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis, + this.maxRetryDelayMillis); } } diff --git a/flowvault/src/main/java/com/skyflow/VaultClient.java b/flowvault/src/main/java/com/skyflow/VaultClient.java index a8a2e2d7..d45d50fc 100644 --- a/flowvault/src/main/java/com/skyflow/VaultClient.java +++ b/flowvault/src/main/java/com/skyflow/VaultClient.java @@ -5,9 +5,9 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; import com.skyflow.generated.rest.ApiClientBuilder; -import com.skyflow.generated.rest.core.RetryInterceptor; import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; import com.skyflow.generated.rest.resources.records.RecordsClient; +import com.skyflow.utils.SkyflowRetryInterceptor; import com.skyflow.utils.Utils; import java.util.concurrent.TimeUnit; @@ -25,10 +25,14 @@ public class VaultClient extends BaseVaultClient { private Integer commonReadTimeout; private Integer commonWriteTimeout; private Integer commonMaxRetries; + private Long commonInitialRetryDelayMillis; + private Long commonMaxRetryDelayMillis; // SDK defaults, used when neither the vault-level nor the client-wide value is set. private static final int DEFAULT_TIMEOUT_SECONDS = 60; // Retries OFF by default (opt-in) so non-idempotent bulk writes aren't replayed automatically. private static final int DEFAULT_MAX_RETRIES = 0; + private static final long DEFAULT_INITIAL_RETRY_DELAY_MILLIS = 500L; + private static final long DEFAULT_MAX_RETRY_DELAY_MILLIS = 2000L; protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException { super(vaultConfig, credentials); @@ -42,12 +46,15 @@ protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws S * client and ApiClient so the next call rebuilds them with the new values. */ protected void setCommonHttpConfig(Integer timeout, Integer connectTimeout, Integer readTimeout, - Integer writeTimeout, Integer maxRetries) { + Integer writeTimeout, Integer maxRetries, + Long initialRetryDelayMillis, Long maxRetryDelayMillis) { this.commonTimeout = timeout; this.commonConnectTimeout = connectTimeout; this.commonReadTimeout = readTimeout; this.commonWriteTimeout = writeTimeout; this.commonMaxRetries = maxRetries; + this.commonInitialRetryDelayMillis = initialRetryDelayMillis; + this.commonMaxRetryDelayMillis = maxRetryDelayMillis; this.sharedHttpClient = null; this.apiClient = null; } @@ -60,6 +67,14 @@ private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defau return clientLevel != null ? clientLevel : defaultValue; } + /** Resolve a long setting: vault-level override, else client-wide default, else the SDK default. */ + private static long resolveLong(Long vaultLevel, Long clientLevel, long defaultValue) { + if (vaultLevel != null) { + return vaultLevel; + } + return clientLevel != null ? clientLevel : defaultValue; + } + /** * Resolve an optional setting: vault-level override, else client-wide default, else null. * Null means "not configured" — the caller leaves the underlying HTTP client default in place. @@ -134,6 +149,10 @@ protected void updateExecutorInHTTP() { if (sharedHttpClient == null) { int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS); int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES); + long initialRetryDelayMillis = resolveLong(vaultConfig.getInitialRetryDelayMillis(), + commonInitialRetryDelayMillis, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); + long maxRetryDelayMillis = resolveLong(vaultConfig.getMaxRetryDelayMillis(), + commonMaxRetryDelayMillis, DEFAULT_MAX_RETRY_DELAY_MILLIS); // Per-attempt timeouts: null => leave OkHttp's built-in default (backward compatible). Integer connectTimeout = resolveNullableInt(vaultConfig.getConnectTimeout(), commonConnectTimeout); Integer readTimeout = resolveNullableInt(vaultConfig.getReadTimeout(), commonReadTimeout); @@ -145,7 +164,7 @@ protected void updateExecutorInHTTP() { .callTimeout(timeoutSeconds, TimeUnit.SECONDS) // OUTER: retries. Must wrap the auth interceptor so each attempt re-reads the // (possibly refreshed) bearer token rather than replaying a stale one. - .addInterceptor(new RetryInterceptor(maxRetries)) + .addInterceptor(new SkyflowRetryInterceptor(maxRetries, initialRetryDelayMillis, maxRetryDelayMillis)) .addInterceptor(chain -> { // INNER: auth Request requestWithAuth = chain.request().newBuilder() .header("Authorization", "Bearer " + this.token) diff --git a/flowvault/src/main/java/com/skyflow/config/VaultConfig.java b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java index b14ed6b5..3369abc7 100644 --- a/flowvault/src/main/java/com/skyflow/config/VaultConfig.java +++ b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java @@ -21,6 +21,8 @@ public class VaultConfig extends BaseVaultConfig { private Integer readTimeout; // per-attempt response-read timeout, in seconds private Integer writeTimeout; // per-attempt request-write timeout, in seconds private Integer maxRetries; // retry attempts after the first failure + private Long initialRetryDelayMillis; // backoff before the first retry, in milliseconds + private Long maxRetryDelayMillis; // ceiling the exponential backoff grows to, in milliseconds public VaultConfig() { super(); @@ -30,6 +32,8 @@ public VaultConfig() { this.readTimeout = null; this.writeTimeout = null; this.maxRetries = null; + this.initialRetryDelayMillis = null; + this.maxRetryDelayMillis = null; } public String getVaultUrl() { @@ -114,4 +118,35 @@ public void setMaxRetries(Integer maxRetries) { this.maxRetries = maxRetries; } + + public Long getInitialRetryDelayMillis() { + return initialRetryDelayMillis; + } + + /** + * Backoff before the first retry, in milliseconds, for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().initialRetryDelayMillis(...)}. + * Leave unset (null) to inherit that value, or the SDK default of 500 ms if it is also unset. + * Only applies when {@code maxRetries} is greater than zero. + */ + public void setInitialRetryDelayMillis(Long initialRetryDelayMillis) { + this.initialRetryDelayMillis = initialRetryDelayMillis; + } + + public Long getMaxRetryDelayMillis() { + return maxRetryDelayMillis; + } + + /** + * Ceiling the exponential backoff grows to, in milliseconds, for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().maxRetryDelayMillis(...)}. + * Leave unset (null) to inherit that value, or the SDK default of 2000 ms if it is also unset. + * Only applies when {@code maxRetries} is greater than zero. + */ + public void setMaxRetryDelayMillis(Long maxRetryDelayMillis) { + this.maxRetryDelayMillis = maxRetryDelayMillis; + } + } diff --git a/flowvault/src/main/java/com/skyflow/utils/SkyflowRetryInterceptor.java b/flowvault/src/main/java/com/skyflow/utils/SkyflowRetryInterceptor.java new file mode 100644 index 00000000..703c698e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/utils/SkyflowRetryInterceptor.java @@ -0,0 +1,97 @@ +package com.skyflow.utils; + +import okhttp3.Interceptor; +import okhttp3.Response; + +import java.io.IOException; +import java.util.Random; + +/** + * Retries failed requests with exponential backoff and jitter. + *

+ * This exists as hand-written code rather than using the generated + * {@code com.skyflow.generated.rest.core.RetryInterceptor} because that one only accepts a retry + * count — it has no way to configure the backoff delays that {@code VaultConfig} exposes. It also + * keeps its backoff counter on the interceptor instance, so a single shared instance exhausts its + * retry budget once for the whole client rather than once per request; this implementation keeps + * that state per call. + *

+ * Retries the same statuses the generated interceptor does: 408, 429, and any 5xx. + */ +public final class SkyflowRetryInterceptor implements Interceptor { + + /** Fraction of the computed delay applied as random jitter, so retries do not align. */ + private static final double JITTER_FACTOR = 0.2; + + private final int maxRetries; + private final long initialRetryDelayMillis; + private final long maxRetryDelayMillis; + private final Random random = new Random(); + + public SkyflowRetryInterceptor(int maxRetries, long initialRetryDelayMillis, long maxRetryDelayMillis) { + if (maxRetries < 0) { + throw new IllegalArgumentException("maxRetries must be non-negative"); + } + if (initialRetryDelayMillis < 0) { + throw new IllegalArgumentException("initialRetryDelayMillis must be non-negative"); + } + if (maxRetryDelayMillis < 0) { + throw new IllegalArgumentException("maxRetryDelayMillis must be non-negative"); + } + this.maxRetries = maxRetries; + this.initialRetryDelayMillis = initialRetryDelayMillis; + this.maxRetryDelayMillis = maxRetryDelayMillis; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + // Retry budget is scoped to this call, not to the interceptor instance. + for (int attempt = 1; attempt <= maxRetries && shouldRetry(response.code()); attempt++) { + sleep(backoffMillis(attempt)); + response.close(); + response = chain.proceed(chain.request()); + } + return response; + } + + /** Exponential growth from the initial delay, capped at the maximum, then jittered. */ + long backoffMillis(int attempt) { + long delay = initialRetryDelayMillis; + for (int i = 1; i < attempt && delay < maxRetryDelayMillis; i++) { + delay = delay > maxRetryDelayMillis / 2 ? maxRetryDelayMillis : delay * 2; + } + delay = Math.min(delay, maxRetryDelayMillis); + long jitter = (long) (delay * JITTER_FACTOR); + if (jitter <= 0) { + return delay; + } + // delay +/- up to JITTER_FACTOR, never negative. + return Math.max(0, delay - jitter + random.nextInt((int) Math.min(2 * jitter + 1, Integer.MAX_VALUE))); + } + + static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + private static void sleep(long millis) throws IOException { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting to retry request", e); + } + } + + public int getMaxRetries() { + return maxRetries; + } + + public long getInitialRetryDelayMillis() { + return initialRetryDelayMillis; + } + + public long getMaxRetryDelayMillis() { + return maxRetryDelayMillis; + } +} diff --git a/flowvault/src/main/java/com/skyflow/utils/Utils.java b/flowvault/src/main/java/com/skyflow/utils/Utils.java index a88b3b15..e4261a00 100644 --- a/flowvault/src/main/java/com/skyflow/utils/Utils.java +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -2,9 +2,7 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import com.google.gson.JsonObject; import com.skyflow.config.VaultConfig; @@ -13,6 +11,7 @@ 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.V1FlowDetokenizeRequest; import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; import com.skyflow.generated.rest.types.FlowEnumUpdateType; @@ -32,6 +31,12 @@ import com.skyflow.logs.ErrorLogs; import com.skyflow.utils.logger.LogUtil; import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.DeleteTokensRecord; +import com.skyflow.vault.data.TokenizeRequestRecord; +import com.skyflow.vault.data.TokenizeResponseToken; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; @@ -39,22 +44,29 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; -import com.skyflow.vault.data.DeleteTokensSuccess; import com.skyflow.vault.data.ErrorRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.TokenGroupRedactions; -import com.skyflow.vault.data.TokenizeSuccess; import com.skyflow.vault.data.UpsertOptions; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + public final class Utils extends BaseUtils { + // Spellings the vault has used for the per-record status, in precedence order. + private static final String[] HTTP_CODE_KEYS = {"http_code", "httpCode", "statusCode"}; + public static String getVaultUrl(String clusterId, Env env) { // The 3-arg overload is inherited from common's BaseUtils, which keeps the older // getVaultURL spelling (shared with v2), so it is qualified rather than renamed here. @@ -68,7 +80,6 @@ public static JsonObject getMetrics() { return details; } - public static String getEnvVaultUrl() throws SkyflowException { try { String vaultUrl = System.getenv("VAULT_URL"); @@ -104,7 +115,6 @@ public static boolean isValidUrl(String url) { } } - // Mirrors the "present" test used by the request validators: null and blank both count as absent. private static boolean hasText(String value) { return value != null && !value.trim().isEmpty(); @@ -127,18 +137,24 @@ private static V1Upsert toV1Upsert(UpsertOptions upsert) { public static V1InsertRequest getInsertRequestBody(InsertRequest request, VaultConfig config) { List records = request.getRecords(); List insertRecordDataList = new ArrayList<>(); + // tableName and upsert must reach the wire at exactly one level: the vault rejects a body + // that carries either at both the request and the record level. validateTableAndUpsertPlacement + // has already forced the caller to pick one, so mirror that choice here rather than copying + // the request-level value down onto every record. for (InsertRequestRecord record : records) { V1InsertRecordData.Builder data = V1InsertRecordData.builder() - .data(record.getData()) - // A blank record-level table name counts as absent, matching - // validateInsertRequest, so it falls back to the request-level one. - .tableName(hasText(record.getTableName()) ? record.getTableName() : request.getTableName()); + .data(record.getData()); + // A blank record-level table name counts as absent, matching validateInsertRequest. + if (hasText(record.getTableName())) { + data.tableName(record.getTableName()); + } if (record.getTokens() != null && !record.getTokens().isEmpty()) { data.tokens(record.getTokens()); } - UpsertOptions upsert = record.getUpsert() != null ? record.getUpsert() : request.getUpsert(); - if (upsert != null && upsert.getUniqueColumns() != null && !upsert.getUniqueColumns().isEmpty()) { - data.upsert(toV1Upsert(upsert)); + UpsertOptions recordUpsert = record.getUpsert(); + if (recordUpsert != null && recordUpsert.getUniqueColumns() != null + && !recordUpsert.getUniqueColumns().isEmpty()) { + data.upsert(toV1Upsert(recordUpsert)); } insertRecordDataList.add(data.build()); } @@ -149,6 +165,11 @@ public static V1InsertRequest getInsertRequestBody(InsertRequest request, VaultC if (hasText(request.getTableName())) { builder.tableName(request.getTableName()); } + UpsertOptions requestUpsert = request.getUpsert(); + if (requestUpsert != null && requestUpsert.getUniqueColumns() != null + && !requestUpsert.getUniqueColumns().isEmpty()) { + builder.upsert(toV1Upsert(requestUpsert)); + } return builder.build(); } @@ -189,13 +210,11 @@ public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDe .build(); } - public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest getBulkTokenizeRequestBody(BulkTokenizeRequest request, String vaultId) { + public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest getBulkTokenizeRequestBody( + List records, String vaultId) { List dataList = new ArrayList<>(); - for (BulkTokenizeRecord record : request.getData()) { - dataList.add(V1FlowTokenizeRequestObject.builder() - .value(record.getValue()) - .tokenGroupNames(record.getTokenGroupNames()) - .build()); + for (BulkTokenizeRequestRecord record : records) { + dataList.add(buildTokenizeRequestObject(record)); } return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() .vaultId(vaultId) @@ -251,43 +270,87 @@ public static List createBulkTokenizeBatches( - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request, int batchSize) { - List batches = new ArrayList<>(); - List data = request.getData().get(); - for (int i = 0; i < data.size(); i += batchSize) { - List batchData = data.subList(i, Math.min(i + batchSize, data.size())); - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest = - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() - .vaultId(request.getVaultId()) - .data(new ArrayList<>(batchData)) - .build(); - batches.add(batchRequest); + /** + * Splits the caller's records into batches, in order and without gaps. + * + *

A batch is closed early when the next record repeats a value already in it. The response + * carries no record identifier, so {@link #formatBulkTokenizeResponse} tells one record's rows + * from the next by watching the value change — which only works while values are distinct within + * a request. Batches stay contiguous, so a batch's records still occupy consecutive positions in + * the caller's list and their indexes follow from the batch's start. + */ + public static List> createBulkTokenizeBatches( + List records, int batchSize) { + List> batches = new ArrayList<>(); + if (records == null || records.isEmpty()) return batches; + List current = new ArrayList<>(); + Set valuesInBatch = new HashSet<>(); + Set valueKeysInBatch = new HashSet<>(); + for (BulkTokenizeRequestRecord record : records) { + Object value = record == null ? null : record.getValue(); + String valueKey = String.valueOf(value); + boolean repeatsValue = valuesInBatch.contains(value) || valueKeysInBatch.contains(valueKey); + if (!current.isEmpty() && (current.size() >= batchSize || repeatsValue)) { + batches.add(current); + current = new ArrayList<>(); + valuesInBatch = new HashSet<>(); + valueKeysInBatch = new HashSet<>(); + } + current.add(record); + valuesInBatch.add(value); + valueKeysInBatch.add(valueKey); } + batches.add(current); return batches; } + // Error bodies routinely carry a key whose value is explicitly null (e.g. "skyflowID": null on a + // failed record), so containsKey() is not enough to know a value is usable — read through these + // helpers. Anything that throws here masks the real server error with a parsing crash. + + /** Value as a String, or null when the key is absent or explicitly null. */ + private static String readString(Map recordMap, String key) { + Object value = recordMap.get(key); + return value == null ? null : value.toString(); + } + + /** First usable HTTP status among the known spellings, else {@code fallback}. */ + private static int readHttpCode(Map recordMap, int fallback) { + for (String key : HTTP_CODE_KEYS) { + Object value = recordMap.get(key); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + try { + return Integer.parseInt(((String) value).trim()); + } catch (NumberFormatException ignored) { + // fall through to the next spelling + } + } + } + return fallback; + } + + /** + * Error text from "error", else "message", else a placeholder. Never null: a null message on an + * error record would make it read as a success downstream, since that is how failures are counted. + */ + private static String readErrorMessage(Map recordMap) { + String error = readString(recordMap, "error"); + if (error != null) { + return error; + } + String message = readString(recordMap, "message"); + return message != null ? message : "Unknown error"; + } + public static BulkInsertResponseRecord createInsertErrorRecord(Map recordMap, int indexNumber, String requestId) { BulkInsertResponseRecord err = null; if (recordMap != null) { - int code = 500; - if (recordMap.containsKey("http_code")) { - code = (Integer) recordMap.get("http_code"); - } else if (recordMap.containsKey("httpCode")) { - code = (Integer) recordMap.get("httpCode"); - } else if (recordMap.containsKey("statusCode")) { - code = (Integer) recordMap.get("statusCode"); - } - // check if skyflowID is present - String skyflowID = null; - if (recordMap.containsKey("skyflowID")) { - skyflowID = recordMap.get("skyflowID").toString(); - } - String tableName = null; - if (recordMap.containsKey("tableName")) { - tableName = recordMap.get("tableName").toString(); - } - String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : - recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + int code = readHttpCode(recordMap, 500); + String skyflowID = readString(recordMap, "skyflowID"); + String tableName = readString(recordMap, "tableName"); + String message = readErrorMessage(recordMap); err = new BulkInsertResponseRecord(indexNumber, tableName, skyflowID, null, null, code, message, requestId); } return err; @@ -296,25 +359,11 @@ public static BulkInsertResponseRecord createInsertErrorRecord(Map recordMap, int indexNumber, String requestId) { BulkDetokenizeResponseRecord err = null; if (recordMap != null) { - int code = 500; - if (recordMap.containsKey("http_code")) { - code = (Integer) recordMap.get("http_code"); - } else if (recordMap.containsKey("httpCode")) { - code = (Integer) recordMap.get("httpCode"); - } else if (recordMap.containsKey("statusCode")) { - code = (Integer) recordMap.get("statusCode"); - } + int code = readHttpCode(recordMap, 500); // the failing token is echoed back so the caller can tell which one it was - String token = null; - if (recordMap.containsKey("token")) { - token = recordMap.get("token").toString(); - } - String tokenGroupName = null; - if (recordMap.containsKey("tokenGroupName")) { - tokenGroupName = recordMap.get("tokenGroupName").toString(); - } - String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : - recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + String token = readString(recordMap, "token"); + String tokenGroupName = readString(recordMap, "tokenGroupName"); + String message = readErrorMessage(recordMap); err = new BulkDetokenizeResponseRecord(indexNumber, token, null, tokenGroupName, null, code, message, requestId); } return err; @@ -323,16 +372,8 @@ public static BulkDetokenizeResponseRecord createDetokenizeErrorRecord(Map recordMap, int indexNumber, String requestId) { ErrorRecord err = null; if (recordMap != null) { - int code = 500; - if (recordMap.containsKey("http_code")) { - code = (Integer) recordMap.get("http_code"); - } else if (recordMap.containsKey("httpCode")) { - code = (Integer) recordMap.get("httpCode"); - } else if (recordMap.containsKey("statusCode")) { - code = (Integer) recordMap.get("statusCode"); - } - String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : - recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + int code = readHttpCode(recordMap, 500); + String message = readErrorMessage(recordMap); err = new ErrorRecord(indexNumber, message, code, requestId); } return err; @@ -398,7 +439,20 @@ public static List handleBulkInsertBatchException( } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { - BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + String message = null; + if (cause != null && cause.getMessage() != null){ + message = cause.getMessage(); + } + if (cause != null && cause.getLocalizedMessage() !=null) { + message = cause.getLocalizedMessage(); + } + if (cause != null && cause.getCause() !=null) { + message = cause.getCause().toString(); + } + if (message == null || message.isEmpty() || message.trim().isEmpty()){ + message = ex.getMessage(); + } + BulkInsertResponseRecord err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -465,8 +519,21 @@ public static List handleBulkDetokenizeBatchExcept } } else { int indexNumber = batchNumber * batchSize; + String message = null; + if (cause != null && cause.getMessage() != null){ + message = cause.getMessage(); + } + if (cause != null && cause.getLocalizedMessage() !=null) { + message = cause.getLocalizedMessage(); + } + if (cause != null && cause.getCause() !=null) { + message = cause.getCause().toString(); + } + if (message == null || message.isEmpty() || message.trim().isEmpty()){ + message = ex.getMessage(); + } for (int j = 0; j < batch.getTokens().get().size(); j++) { - BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, ex.getMessage(), null); + BulkDetokenizeResponseRecord err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, 500, message, null); allRecords.add(err); indexNumber++; } @@ -474,30 +541,32 @@ public static List handleBulkDetokenizeBatchExcept return allRecords; } - public static List handleBulkDeleteTokensBatchException( + public static List handleBulkDeleteTokensBatchException( Throwable ex, com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch, int batchNumber, int batchSize ) { - List errorRecords = new ArrayList<>(); + List errorRecords = new ArrayList<>(); + List batchTokens = (batch != null && batch.getTokens().isPresent()) + ? batch.getTokens().get() : new ArrayList<>(); + int startIndex = batchNumber * batchSize; Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; String requestId = extractRequestId(apiException.headers()); Object rawBody = apiException.body(); Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; - int indexNumber = batchNumber * batchSize; if (responseBody != null) { if (responseBody.containsKey("tokens")) { Object tokensList = responseBody.get("tokens"); if (tokensList instanceof List) { List recordsList = (List) tokensList; - for (Object record : recordsList) { + for (int position = 0; position < recordsList.size(); position++) { + Object record = recordsList.get(position); if (record instanceof Map) { Map recordMap = (Map) record; - ErrorRecord err = createErrorRecord(recordMap, indexNumber, requestId); - errorRecords.add(err); - indexNumber++; + errorRecords.add(createDeleteTokensErrorRecord(recordMap, + startIndex + position, tokenAt(batchTokens, position), requestId)); } } } @@ -505,94 +574,155 @@ public static List handleBulkDeleteTokensBatchException( Object errField = responseBody.get("error"); Map recordMap = (errField instanceof Map) ? (Map) errField : null; String fallbackMsg = (errField instanceof String) ? (String) errField : null; - int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; - for (int j = 0; j < tokenCount; j++) { - ErrorRecord err = (recordMap != null) - ? createErrorRecord(recordMap, indexNumber, requestId) - : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); - errorRecords.add(err); - indexNumber++; + for (int position = 0; position < batchTokens.size(); position++) { + errorRecords.add((recordMap != null) + ? createDeleteTokensErrorRecord(recordMap, startIndex + position, + tokenAt(batchTokens, position), requestId) + : new BulkDeleteTokensResponseRecord( + startIndex + position, tokenAt(batchTokens, position), + apiException.statusCode(), + fallbackMsg != null ? fallbackMsg : apiException.getMessage(), + requestId)); } } } if (errorRecords.isEmpty()) { - int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; - for (int j = 0; j < tokenCount; j++) { - errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); - indexNumber++; + for (int position = 0; position < batchTokens.size(); position++) { + errorRecords.add(new BulkDeleteTokensResponseRecord( + startIndex + position, tokenAt(batchTokens, position), + apiException.statusCode(), apiException.getMessage(), requestId)); } } } else { - int indexNumber = batchNumber * batchSize; - for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); - indexNumber++; + // a transport-level failure never reached the API, so there is no id to report + for (int position = 0; position < batchTokens.size(); position++) { + errorRecords.add(new BulkDeleteTokensResponseRecord( + startIndex + position, tokenAt(batchTokens, position), 500, ex.getMessage())); } } return errorRecords; } - public static List handleBulkTokenizeBatchException( - Throwable ex, - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch, - int batchNumber, int batchSize - ) { - List errorRecords = new ArrayList<>(); + private static String tokenAt(List tokens, int position) { + return (tokens != null && position < tokens.size()) ? tokens.get(position) : null; + } + + private static BulkDeleteTokensResponseRecord createDeleteTokensErrorRecord( + Map recordMap, int index, String requestedToken, String requestId) { + int code = 500; + if (recordMap.containsKey("http_code")) { + code = (Integer) recordMap.get("http_code"); + } else if (recordMap.containsKey("httpCode")) { + code = (Integer) recordMap.get("httpCode"); + } else if (recordMap.containsKey("statusCode")) { + code = (Integer) recordMap.get("statusCode"); + } + String message = recordMap.containsKey("error") ? (String) recordMap.get("error") : + recordMap.containsKey("message") ? (String) recordMap.get("message") : "Unknown error"; + Object echoedToken = recordMap.get("value"); + String token = (echoedToken instanceof String) ? (String) echoedToken : requestedToken; + return new BulkDeleteTokensResponseRecord(index, token, code, message, requestId); + } + + public static List handleBulkTokenizeBatchException( + Throwable ex, List batchRecords, int startIndex) { + String message; + int httpCode; + String requestId = null; Throwable cause = ex.getCause(); if (cause instanceof ApiClientApiException) { ApiClientApiException apiException = (ApiClientApiException) cause; - String requestId = extractRequestId(apiException.headers()); - Object rawBody = apiException.body(); - Map responseBody = (rawBody instanceof Map) ? (Map) rawBody : null; - int indexNumber = batchNumber * batchSize; - if (responseBody != null) { - if (responseBody.containsKey("response")) { - Object responseList = responseBody.get("response"); - if (responseList instanceof List) { - List recordsList = (List) responseList; - for (Object record : recordsList) { - if (record instanceof Map) { - Map recordMap = (Map) record; - ErrorRecord err = createErrorRecord(recordMap, indexNumber, requestId); - errorRecords.add(err); - indexNumber++; - } - } - } - } else if (responseBody.containsKey("error")) { - Object errField = responseBody.get("error"); - Map recordMap = (errField instanceof Map) ? (Map) errField : null; - String fallbackMsg = (errField instanceof String) ? (String) errField : null; - int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; - for (int j = 0; j < batchDataSize; j++) { - ErrorRecord err = (recordMap != null) - ? createErrorRecord(recordMap, indexNumber, requestId) - : new ErrorRecord(indexNumber, fallbackMsg != null ? fallbackMsg : apiException.getMessage(), apiException.statusCode(), requestId); - errorRecords.add(err); - indexNumber++; - } - } - } - if (errorRecords.isEmpty()) { - int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; - for (int j = 0; j < batchDataSize; j++) { - errorRecords.add(new ErrorRecord(indexNumber, apiException.getMessage(), apiException.statusCode(), requestId)); - indexNumber++; - } + // a rejected request still describes each record in its body - prefer that detail over + // the bare status code, and only synthesise entries when there is nothing to read + List fromBody = + tokenizeRecordsFromErrorBody(apiException, batchRecords, startIndex); + if (fromBody != null) { + return fromBody; } + httpCode = apiException.statusCode(); + message = extractBatchErrorMessage(apiException); + requestId = extractRequestId(apiException.headers()); } else { - int indexNumber = batchNumber * batchSize; - int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; - for (int j = 0; j < batchDataSize; j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); - indexNumber++; + // a transport-level failure never reached the API, so there is no id to report + httpCode = 500; + message = ex.getMessage(); + } + // a batch-level failure fails every token group of every value in that batch + List errorRecords = new ArrayList<>(); + if (batchRecords == null) return errorRecords; + for (int position = 0; position < batchRecords.size(); position++) { + BulkTokenizeRequestRecord requested = batchRecords.get(position); + List tokens = new ArrayList<>(); + List groupNames = requested.getTokenGroupNames(); + if (groupNames == null || groupNames.isEmpty()) { + tokens.add(new TokenizeResponseToken(null, null, httpCode, message, requestId)); + } else { + for (String groupName : groupNames) { + tokens.add(new TokenizeResponseToken(groupName, null, httpCode, message, requestId)); + } } + errorRecords.add(new BulkTokenizeResponseRecord( + startIndex + position, requested.getValue(), tokens)); } return errorRecords; } + /** + * Rebuilds the response records from a rejected request's body. + * + *

The API answers a 4xx with the same {@code response} array it would have returned on + * success, one row per rejected (value, token group) carrying its own message - for example + * "Invalid request. BYOT token should contain one token group." Reading it keeps the caller's + * error identical whether the request was rejected outright or reported per record. + * + * @return null when the body is absent or in an unfamiliar shape, so the caller can fall back + * to summarising the batch by its status code + */ + private static List tokenizeRecordsFromErrorBody( + ApiClientApiException apiException, + List batchRecords, + int startIndex) { + Object rawBody = apiException.body(); + if (!(rawBody instanceof Map) || !((Map) rawBody).containsKey("response")) { + return null; + } + try { + V1FlowTokenizeResponse parsed = + ObjectMappers.JSON_MAPPER.convertValue(rawBody, V1FlowTokenizeResponse.class); + if (!parsed.getResponse().isPresent() || parsed.getResponse().get().isEmpty()) { + return null; + } + return groupTokenizeRows(parsed.getResponse().get(), batchRecords, startIndex, + extractRequestId(apiException.headers())); + } catch (RuntimeException ignored) { + // body did not deserialise into the shape we know; let the caller summarise instead + return null; + } + } + + /** Pulls the most specific message available from a failed batch response body. */ + private static String extractBatchErrorMessage(ApiClientApiException apiException) { + Object rawBody = apiException.body(); + if (rawBody instanceof Map) { + Object errField = ((Map) rawBody).get("error"); + if (errField instanceof String) { + return (String) errField; + } + if (errField instanceof Map) { + Map errMap = (Map) errField; + Object message = errMap.containsKey("error") ? errMap.get("error") : errMap.get("message"); + if (message instanceof String) { + return (String) message; + } + } + } + return apiException.getMessage(); + } + + private static BulkTokenizeRequestRecord recordAt(List records, int position) { + return (records != null && position < records.size()) ? records.get(position) : null; + } + public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse response, int batch, int batchSize, Map> headers) { BulkInsertResponse formattedResponse = null; List records = new ArrayList<>(); @@ -602,6 +732,10 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { V1RecordResponseObject current = record.get(index); + String reqID = null; + if(current.getError().isPresent()){ + reqID = extractRequestId(headers); + } records.add(new BulkInsertResponseRecord( indexNumber, current.getTableName().orElse(null), @@ -610,7 +744,7 @@ public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse respo current.getHashedData().orElse(null), current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), - null)); + reqID)); indexNumber++; } formattedResponse = new BulkInsertResponse(records); @@ -626,15 +760,27 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni int recordsSize = record.size(); for (int index = 0; index < recordsSize; index++) { V1FlowDetokenizeResponseObject current = record.get(index); + Map data = null; + if(current.getMetadata().isPresent()){ + data = current.getMetadata().get(); + if (data.containsKey("skyflowID")) { + Object value = data.remove("skyflowID"); + data.put("skyflowId", value); + } + } + String reqID = null; + if(current.getError().isPresent()){ + reqID = extractRequestId(headers); + } records.add(new BulkDetokenizeResponseRecord( indexNumber, current.getToken().orElse(null), current.getValue().orElse(null), current.getTokenGroupName().orElse(null), - current.getMetadata().orElse(null), + data, current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), current.getError().orElse(null), - null)); + reqID)); indexNumber++; } return new BulkDetokenizeResponse(records); @@ -643,66 +789,224 @@ public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokeni } public static BulkDeleteTokensResponse formatBulkDeleteTokensResponse( - V1FlowDeleteTokenResponse response, int batch, int batchSize, Map> headers) { + V1FlowDeleteTokenResponse response, + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batchRequest, + int batch, int batchSize, Map> headers) { if (response != null && response.getTokens().isPresent()) { - String requestId = extractRequestId(headers); List records = response.getTokens().get(); - List errorRecords = new ArrayList<>(); - List successRecords = new ArrayList<>(); + List requestedTokens = batchRequest != null && batchRequest.getTokens().isPresent() + ? batchRequest.getTokens().get() : null; + List responseRecords = new ArrayList<>(); int indexNumber = batch * batchSize; - for (V1DeleteTokenResponseObject record : records) { - String tokenValue = record.getValue().orElse(null); - if (record.getError().isPresent() - && record.getError().get() != null - && !record.getError().get().isEmpty() - && record.getHttpCode().orElse(200) != 200) { - ErrorRecord errorRecord = new ErrorRecord(indexNumber, record.getError().get(), - record.getHttpCode().orElse(500), requestId); - errorRecords.add(errorRecord); - } else { - DeleteTokensSuccess success = new DeleteTokensSuccess(indexNumber, tokenValue); - successRecords.add(success); - } + // one id per API call, so every error this batch reports carries the same one + String requestId = extractRequestId(headers); + for (int position = 0; position < records.size(); position++) { + V1DeleteTokenResponseObject record = records.get(position); + boolean failed = isFailedRecord(record); + // The API echoes the token back on both paths, but fall back to the token we sent at + // this position so an error record is never missing its token. + String tokenValue = record.getValue().orElse( + requestedTokens != null && position < requestedTokens.size() + ? requestedTokens.get(position) : null); + responseRecords.add(new BulkDeleteTokensResponseRecord( + indexNumber, + tokenValue, + record.getHttpCode().orElse(failed ? 500 : 200), + failed ? record.getError().get() : null, + requestId + )); indexNumber++; } - return new BulkDeleteTokensResponse(successRecords, errorRecords); + return new BulkDeleteTokensResponse(responseRecords); } return null; } + /** Converts one wire record into the unified success/error record shape. */ + /** Maps one SDK request record to the wire object, carrying the BYOT token when supplied. */ + private static V1FlowTokenizeRequestObject buildTokenizeRequestObject(TokenizeRequestRecord record) { + V1FlowTokenizeRequestObject.Builder builder = V1FlowTokenizeRequestObject.builder() + .value(record.getValue()) + .tokenGroupNames(record.getTokenGroupNames()); + if (record.getToken() != null) { + builder = builder.token(record.getToken()); + } + return builder.build(); + } + + private static List buildTokenizeResponseTokens( + V1FlowTokenizeResponseObject record, String requestId) { + List tokens = new ArrayList<>(); + if (record.getTokens().isPresent()) { + for (FlowTokenizeResponseObjectToken tokenObj : record.getTokens().get()) { + boolean failed = tokenObj.getError().isPresent() + && tokenObj.getError().get() != null + && !tokenObj.getError().get().isEmpty(); + tokens.add(new TokenizeResponseToken( + tokenObj.getTokenGroupName().orElse(null), + tokenObj.getToken().orElse(null), + tokenObj.getHttpCode().orElse(failed ? 500 : 200), + failed ? tokenObj.getError().get() : null, + requestId + )); + } + } else { + // the API reports one flat row per (value, token group) instead of a nested tokens + // array; the generated type has no fields for those, so they land in additionalProperties + TokenizeResponseToken flat = flatToken(record, requestId); + if (flat != null) { + tokens.add(flat); + } + } + return tokens; + } + + /** + * Reads a flat {@code tokenGroupName}/{@code token}/{@code error}/{@code httpCode} row out of + * the wire object's unmodelled properties. Returns null when the row carries none of them, so a + * genuinely token-less record still reports an empty list rather than a phantom entry. + */ + private static TokenizeResponseToken flatToken(V1FlowTokenizeResponseObject record, String requestId) { + Map extras = record.getAdditionalProperties(); + if (extras == null || extras.isEmpty()) { + return null; + } + boolean carriesTokenFields = extras.containsKey("token") + || extras.containsKey("tokenGroupName") + || extras.containsKey("error") + || extras.containsKey("httpCode"); + if (!carriesTokenFields) { + return null; + } + String error = asNonEmptyString(extras.get("error")); + String token = asNonEmptyString(extras.get("token")); + Integer httpCode = extras.get("httpCode") instanceof Number + ? ((Number) extras.get("httpCode")).intValue() + : (error != null ? 500 : 200); + return new TokenizeResponseToken( + asNonEmptyString(extras.get("tokenGroupName")), token, httpCode, error, requestId); + } + + /** The API sends "" for a token or error that does not apply; normalise both to null. */ + private static String asNonEmptyString(Object value) { + if (!(value instanceof String)) { + return null; + } + String text = (String) value; + return text.isEmpty() ? null : text; + } + + /** + * A record counts as failed only when the API returned a non-empty error message together with + * a non-2xx status, mirroring the check the bulk path has always used. + */ + private static boolean isFailedRecord(V1DeleteTokenResponseObject record) { + return record.getError().isPresent() + && record.getError().get() != null + && !record.getError().get().isEmpty() + && record.getHttpCode().orElse(200) != 200; + } + public static BulkTokenizeResponse formatBulkTokenizeResponse( V1FlowTokenizeResponse response, - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batchRequest, - int batchNumber, int batchSize, Map> headers) { + List batchRecords, + int startIndex, + Map> headers) { if (response != null && response.getResponse().isPresent()) { + List rows = response.getResponse().get(); + // one id per API call, so every error this batch reports carries the same one String requestId = extractRequestId(headers); - List records = response.getResponse().get(); - List successRecords = new ArrayList<>(); - List errorRecords = new ArrayList<>(); - int indexNumber = batchNumber * batchSize; - for (V1FlowTokenizeResponseObject record : records) { - Object value = record.getValue().orElse(null); - TokenizeSuccess successEntry = null; - if (record.getTokens().isPresent()) { - for (FlowTokenizeResponseObjectToken tokenObj : record.getTokens().get()) { - if (tokenObj.getError().isPresent()) { - errorRecords.add(new ErrorRecord(indexNumber, tokenObj.getError().get(), tokenObj.getHttpCode().orElse(500), requestId)); - } else if (tokenObj.getTokenGroupName().isPresent() && tokenObj.getToken().isPresent()) { - if (successEntry == null) { - successEntry = new TokenizeSuccess(indexNumber, value); - } - successEntry.addToken(tokenObj.getTokenGroupName().get(), tokenObj.getToken().get()); - } - } - } - if (successEntry != null) { - successRecords.add(successEntry); - } - indexNumber++; - } - return new BulkTokenizeResponse(successRecords, errorRecords); + return new BulkTokenizeResponse(groupTokenizeRows(rows, batchRecords, startIndex, requestId)); } return null; } + /** + * Folds the response rows back onto the records that produced them. + * + *

The API emits one row per (value, token group) rather than one per record, and a record + * rejected outright yields a single row instead of one per group — so row count is not a + * function of the request. Rows do arrive in request order, though, and each carries its value, + * which is enough: a row belongs to the record being filled while it matches that record's value + * and the record has not yet taken as many rows as it asked for token groups. Anything else + * starts the next record. Batching keeps values distinct within a request (see + * {@link #createBulkTokenizeBatches}), so the value comparison never straddles two records. + * + *

A response already grouped one-row-per-record folds through this unchanged, since each row + * then matches exactly one record before the value moves on. + */ + private static List groupTokenizeRows( + List rows, + List batchRecords, + int startIndex, + String requestId) { + List responseRecords = new ArrayList<>(); + if (batchRecords == null || batchRecords.isEmpty()) { + // nothing to correlate against; fall back to one record per row + for (int position = 0; position < rows.size(); position++) { + responseRecords.add(new BulkTokenizeResponseRecord(startIndex + position, + rows.get(position).getValue().orElse(null), + buildTokenizeResponseTokens(rows.get(position), requestId))); + } + return responseRecords; + } + + int recordPosition = 0; + int rowsTakenByRecord = 0; + List tokens = new ArrayList<>(); + for (V1FlowTokenizeResponseObject row : rows) { + Object rowValue = row.getValue().orElse(null); + while (recordPosition < batchRecords.size() + && !acceptsRow(batchRecords.get(recordPosition), rowValue, rowsTakenByRecord)) { + responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, + batchRecords.get(recordPosition).getValue(), tokens)); + tokens = new ArrayList<>(); + rowsTakenByRecord = 0; + recordPosition++; + } + if (recordPosition >= batchRecords.size()) { + // more rows than the request can account for; keep them rather than drop them + responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, + rowValue, buildTokenizeResponseTokens(row, requestId))); + recordPosition++; + continue; + } + tokens.addAll(buildTokenizeResponseTokens(row, requestId)); + rowsTakenByRecord++; + } + // close the record in flight, then any records the response never mentioned + while (recordPosition < batchRecords.size()) { + responseRecords.add(new BulkTokenizeResponseRecord(startIndex + recordPosition, + batchRecords.get(recordPosition).getValue(), tokens)); + tokens = new ArrayList<>(); + recordPosition++; + } + return responseRecords; + } + + /** A record takes a row while the value still matches and it has room for another token group. */ + private static boolean acceptsRow(BulkTokenizeRequestRecord record, Object rowValue, int rowsTaken) { + List groups = record.getTokenGroupNames(); + int capacity = (groups == null || groups.isEmpty()) ? 1 : groups.size(); + if (rowsTaken >= capacity) { + return false; + } + // the API echoes the value back; a row that omits it can only belong to the record in flight + return rowValue == null || valuesMatch(record.getValue(), rowValue); + } + + /** + * Compares a requested value with the one echoed back. JSON round-tripping turns numbers into + * Double/Integer and objects into Maps, so fall back to string form when equals() disagrees. + */ + private static boolean valuesMatch(Object requested, Object echoed) { + if (Objects.equals(requested, echoed)) { + return true; + } + if (requested == null || echoed == null) { + return false; + } + return String.valueOf(requested).equals(String.valueOf(echoed)); + } + } 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 2be203c4..39565674 100644 --- a/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -411,17 +412,18 @@ public static void validateBulkTokenizeRequest(BulkTokenizeRequest request) thro )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokenizeRequestNull.getMessage()); } - List data = request.getData(); - if (data == null || data.isEmpty()) { + List records = request.getRecords(); + if (records == null || records.isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.EMPTY_TOKENIZE_DATA.getLog(), InterfaceName.TOKENIZE.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenizeData.getMessage()); } - validateBulkDataSize(data.size(), ErrorLogs.TOKENIZE_DATA_SIZE_EXCEED, + validateBulkDataSize(records.size(), ErrorLogs.TOKENIZE_DATA_SIZE_EXCEED, ErrorMessage.TokenizeDataSizeExceedError, InterfaceName.TOKENIZE); - for (int i = 0; i < data.size(); i++) { - BulkTokenizeRecord record = data.get(i); + + for (int i = 0; i < records.size(); i++) { + BulkTokenizeRequestRecord record = records.get(i); if (record == null) { LogUtil.printErrorLog(Utils.parameterizedString( ErrorLogs.TOKENIZE_RECORD_NULL.getLog(), InterfaceName.TOKENIZE.getName() 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 cbe697e6..d405e1b4 100644 --- a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -2,7 +2,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.function.Function; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -22,6 +24,7 @@ import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1Upsert; import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; @@ -30,7 +33,13 @@ import com.skyflow.utils.Utils; import com.skyflow.utils.logger.LogUtil; import com.skyflow.utils.validations.Validations; +import com.skyflow.vault.data.BulkDeleteTokensOptions; +import com.skyflow.vault.data.BulkTokenizeOptions; import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.TokenizeResponseToken; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; @@ -38,19 +47,16 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; import com.skyflow.vault.data.BulkInsertResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeOptions; +import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.DeleteTokensOptions; -import com.skyflow.vault.data.DeleteTokensSuccess; -import com.skyflow.vault.data.DetokenizeOptions; import com.skyflow.vault.data.ErrorRecord; -import com.skyflow.vault.data.InsertOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestContext; import com.skyflow.vault.data.RequestInterceptor; import com.skyflow.vault.data.TokenizeOptions; -import com.skyflow.vault.data.TokenizeSuccess; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; @@ -92,7 +98,7 @@ public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws Sky return bulkInsert(insertRequest, null); } - public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException { + public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); @@ -122,7 +128,7 @@ public CompletableFuture bulkInsertAsync(BulkInsertRequest i return bulkInsertAsync(insertRequest, null); } - public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, InsertOptions options) throws SkyflowException { + public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); @@ -160,7 +166,7 @@ public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeReq return bulkDetokenize(detokenizeRequest, null); } - public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { + public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); @@ -187,7 +193,7 @@ public CompletableFuture bulkDetokenizeAsync(BulkDetoken return bulkDetokenizeAsync(detokenizeRequest, null); } - public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, DetokenizeOptions options) throws SkyflowException { + public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); ExecutorService executor = null; try { @@ -240,7 +246,7 @@ public BulkDeleteTokensResponse bulkDeleteTokens(BulkDeleteTokensRequest deleteT return bulkDeleteTokens(deleteTokensRequest, null); } - public BulkDeleteTokensResponse bulkDeleteTokens(BulkDeleteTokensRequest deleteTokensRequest, DeleteTokensOptions options) throws SkyflowException { + public BulkDeleteTokensResponse bulkDeleteTokens(BulkDeleteTokensRequest deleteTokensRequest, BulkDeleteTokensOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATE_DELETE_TOKENS_REQUEST.getLog()); @@ -265,7 +271,7 @@ public CompletableFuture bulkDeleteTokensAsync(BulkDel return bulkDeleteTokensAsync(deleteTokensRequest, null); } - public CompletableFuture bulkDeleteTokensAsync(BulkDeleteTokensRequest deleteTokensRequest, DeleteTokensOptions options) throws SkyflowException { + public CompletableFuture bulkDeleteTokensAsync(BulkDeleteTokensRequest deleteTokensRequest, BulkDeleteTokensOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); ExecutorService executor = null; try { @@ -279,8 +285,7 @@ public CompletableFuture bulkDeleteTokensAsync(BulkDel LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorTokens = Collections.synchronizedList(new ArrayList<>()); - List successRecords = Collections.synchronizedList(new ArrayList<>()); + List responseRecords = Collections.synchronizedList(new ArrayList<>()); List batches = Utils.createBulkDeleteTokensBatches(request, cfg.batchSize); @@ -293,17 +298,13 @@ public CompletableFuture bulkDeleteTokensAsync(BulkDel .thenApply(v -> { for (CompletableFuture future : futures) { BulkDeleteTokensResponse futureResponse = future.join(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorTokens.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); } } LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_REQUEST_RESOLVED.getLog()); - return new BulkDeleteTokensResponse(successRecords, errorTokens, deleteTokensRequest.getTokens()); + return new BulkDeleteTokensResponse( + sortByIndex(responseRecords), deleteTokensRequest.getTokens()); }); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); @@ -326,17 +327,15 @@ public BulkTokenizeResponse bulkTokenize(BulkTokenizeRequest tokenizeRequest) th return bulkTokenize(tokenizeRequest, null); } - public BulkTokenizeResponse bulkTokenize(BulkTokenizeRequest tokenizeRequest, TokenizeOptions options) throws SkyflowException { + public BulkTokenizeResponse bulkTokenize(BulkTokenizeRequest tokenizeRequest, BulkTokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); try { LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); Validations.validateBulkTokenizeRequest(tokenizeRequest); - BatchConfig cfg = configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + BatchConfig cfg = configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getRecords().size()); setBearerToken(); - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = - Utils.getBulkTokenizeRequestBody(tokenizeRequest, this.getVaultConfig().getVaultId()); RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; - return this.processBulkTokenizeSync(request, tokenizeRequest.getData(), interceptor, cfg); + return this.processBulkTokenizeSync(tokenizeRequest.getRecords(), interceptor, cfg); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); @@ -354,45 +353,38 @@ public CompletableFuture bulkTokenizeAsync(BulkTokenizeReq return bulkTokenizeAsync(tokenizeRequest, null); } - public CompletableFuture bulkTokenizeAsync(BulkTokenizeRequest tokenizeRequest, TokenizeOptions options) throws SkyflowException { + public CompletableFuture bulkTokenizeAsync(BulkTokenizeRequest tokenizeRequest, BulkTokenizeOptions options) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.TOKENIZE_TRIGGERED.getLog()); ExecutorService executor = null; try { LogUtil.printInfoLog(InfoLogs.VALIDATING_TOKENIZE_REQUEST.getLog()); Validations.validateBulkTokenizeRequest(tokenizeRequest); - BatchConfig cfg = configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getData().size()); + BatchConfig cfg = configureTokenizeConcurrencyAndBatchSize(tokenizeRequest.getRecords().size()); setBearerToken(); - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest request = - Utils.getBulkTokenizeRequestBody(tokenizeRequest, this.getVaultConfig().getVaultId()); RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List successRecords = Collections.synchronizedList(new ArrayList<>()); + List responseRecords = Collections.synchronizedList(new ArrayList<>()); - List batches = - Utils.createBulkTokenizeBatches(request, cfg.batchSize); + List> batches = + Utils.createBulkTokenizeBatches(tokenizeRequest.getRecords(), cfg.batchSize); executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); List> futures = - this.tokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); + this.tokenizeBatchFutures(executor, batches, interceptor); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> { for (CompletableFuture future : futures) { BulkTokenizeResponse futureResponse = future.join(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorRecords.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); } } LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); - return new BulkTokenizeResponse(successRecords, errorRecords, tokenizeRequest.getData()); + return new BulkTokenizeResponse( + sortTokenizeByIndex(responseRecords), tokenizeRequest.getRecords()); }); } catch (ApiClientApiException e) { String bodyString = gson.toJson(e.body()); @@ -418,8 +410,7 @@ private BulkDeleteTokensResponse processBulkDeleteTokensSync( BatchConfig cfg ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List successRecords = new ArrayList<>(); + List responseRecords = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); List batches = Utils.createBulkDeleteTokensBatches(deleteTokensRequest, cfg.batchSize); @@ -433,9 +424,8 @@ private BulkDeleteTokensResponse processBulkDeleteTokensSync( } for (CompletableFuture future : futures) { BulkDeleteTokensResponse futureResponse = future.get(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) successRecords.addAll(futureResponse.getSuccess()); - if (futureResponse.getErrors() != null) errorRecords.addAll(futureResponse.getErrors()); + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); } } } catch (Exception e) { @@ -444,11 +434,22 @@ private BulkDeleteTokensResponse processBulkDeleteTokensSync( } finally { executor.shutdown(); } - BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(successRecords, errorRecords, originalTokens); + BulkDeleteTokensResponse response = + new BulkDeleteTokensResponse(sortByIndex(responseRecords), originalTokens); LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_REQUEST_RESOLVED.getLog()); return response; } + /** + * Batches complete concurrently, so order the unified records by their position in the original + * request before handing them back to the caller. + */ + private static List sortByIndex(List records) { + List sorted = new ArrayList<>(records); + sorted.sort(Comparator.comparingInt(BulkDeleteTokensResponseRecord::getIndex)); + return sorted; + } + private List> deleteTokensBatchFutures( ExecutorService executor, List batches, @@ -465,11 +466,12 @@ private List> deleteTokensBatchFutur .supplyAsync(() -> processDeleteTokensBatch(batch, ctx), executor) .handle((result, ex) -> { if (ex != null) { - List batchErrors = + List batchErrors = Utils.handleBulkDeleteTokensBatchException(ex, batch, index, batchSize); - return new BulkDeleteTokensResponse(new ArrayList<>(), batchErrors); + return new BulkDeleteTokensResponse(batchErrors); } - return Utils.formatBulkDeleteTokensResponse(result.body(), index, batchSize, result.headers()); + return Utils.formatBulkDeleteTokensResponse( + result.body(), batch, index, batchSize, result.headers()); }); futures.add(future); } @@ -482,24 +484,32 @@ private ApiClientHttpResponse processDeleteTokensBatc return this.getRecordsApi().withRawResponse().deletetoken(batch, buildRequestOptions(ctx)); } + /** + * Resolves a user-tunable batching setting: process environment first, then a {@code .env} file. + * Package-private and swappable purely so tests can drive batching and concurrency without + * mutating the JVM environment — deliberately not public, so it stays out of the frozen + * public API surface. + */ + static Function settingResolver = VaultController::resolveSettingFromEnvironment; + + private static String resolveSettingFromEnvironment(String key) { + String value = System.getenv(key); + if (value == null) { + try { + value = Dotenv.load().get(key); + } catch (DotenvException ignored) { + // no .env available — environment-only + } + } + return value; + } + private BatchConfig configureDeleteTokensConcurrencyAndBatchSize(int totalRequests) { int batchSize = Constants.DELETE_TOKENS_BATCH_SIZE; int concurrencyLimit; try { - String userProvidedBatchSize = System.getenv("DELETE_TOKENS_BATCH_SIZE"); - String userProvidedConcurrencyLimit = System.getenv("DELETE_TOKENS_CONCURRENCY_LIMIT"); - - Dotenv dotenv = null; - try { - dotenv = Dotenv.load(); - } catch (DotenvException ignored) {} - - if (userProvidedBatchSize == null && dotenv != null) { - userProvidedBatchSize = dotenv.get("DELETE_TOKENS_BATCH_SIZE"); - } - if (userProvidedConcurrencyLimit == null && dotenv != null) { - userProvidedConcurrencyLimit = dotenv.get("DELETE_TOKENS_CONCURRENCY_LIMIT"); - } + String userProvidedBatchSize = settingResolver.apply("DELETE_TOKENS_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("DELETE_TOKENS_CONCURRENCY_LIMIT"); if (userProvidedBatchSize != null) { try { @@ -551,35 +561,27 @@ private BatchConfig configureDeleteTokensConcurrencyAndBatchSize(int totalReques } private BulkTokenizeResponse processBulkTokenizeSync( - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest tokenizeRequest, - ArrayList originalData, + List originalRecords, RequestInterceptor interceptor, BatchConfig cfg ) throws ExecutionException, InterruptedException, SkyflowException { LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); - List errorRecords = Collections.synchronizedList(new ArrayList<>()); - List successRecords = new ArrayList<>(); + List responseRecords = new ArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); - List batches = - Utils.createBulkTokenizeBatches(tokenizeRequest, cfg.batchSize); + List> batches = + Utils.createBulkTokenizeBatches(originalRecords, cfg.batchSize); try { List> futures = - this.tokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); + this.tokenizeBatchFutures(executor, batches, interceptor); try { - CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); - allFutures.join(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } catch (Exception e) { // individual batch errors are already captured } for (CompletableFuture future : futures) { BulkTokenizeResponse futureResponse = future.get(); - if (futureResponse != null) { - if (futureResponse.getSuccess() != null) { - successRecords.addAll(futureResponse.getSuccess()); - } - if (futureResponse.getErrors() != null) { - errorRecords.addAll(futureResponse.getErrors()); - } + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); } } } catch (Exception e) { @@ -588,32 +590,44 @@ private BulkTokenizeResponse processBulkTokenizeSync( } finally { executor.shutdown(); } - BulkTokenizeResponse response = new BulkTokenizeResponse(successRecords, errorRecords, originalData); + BulkTokenizeResponse response = + new BulkTokenizeResponse(sortTokenizeByIndex(responseRecords), originalRecords); LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); return response; } + /** Batches complete concurrently; order results by the index the SDK assigned each record. */ + private static List sortTokenizeByIndex(List records) { + List sorted = new ArrayList<>(records); + sorted.sort(Comparator.comparingInt(BulkTokenizeResponseRecord::getIndex)); + return sorted; + } + private List> tokenizeBatchFutures( ExecutorService executor, - List batches, - RequestInterceptor interceptor, - int batchSize) { + List> batches, + RequestInterceptor interceptor) { List> futures = new ArrayList<>(); if (batches == null) return futures; - for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { - final int index = batchIndex; - com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = batches.get(index); + // batches are contiguous but not uniformly sized - a batch is cut short when it would + // otherwise repeat a value - so track where each one starts rather than deriving it + int nextStartIndex = 0; + for (List batchRecords : batches) { + final int startIndex = nextStartIndex; + nextStartIndex += batchRecords.size(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + Utils.getBulkTokenizeRequestBody(batchRecords, this.getVaultConfig().getVaultId()); RequestContext ctx = new RequestContext("TOKENIZE"); if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture .supplyAsync(() -> processTokenizeBatch(batch, ctx), executor) .handle((result, ex) -> { if (ex != null) { - List batchErrors = - Utils.handleBulkTokenizeBatchException(ex, batch, index, batchSize); - return new BulkTokenizeResponse(new ArrayList<>(), batchErrors); + return new BulkTokenizeResponse(Utils.handleBulkTokenizeBatchException( + ex, batchRecords, startIndex)); } - return Utils.formatBulkTokenizeResponse(result.body(), batch, index, batchSize, result.headers()); + return Utils.formatBulkTokenizeResponse( + result.body(), batchRecords, startIndex, result.headers()); }); futures.add(future); } @@ -630,20 +644,8 @@ private BatchConfig configureTokenizeConcurrencyAndBatchSize(int totalRequests) int batchSize = Constants.TOKENIZE_BATCH_SIZE; int concurrencyLimit; try { - String userProvidedBatchSize = System.getenv("TOKENIZE_BATCH_SIZE"); - String userProvidedConcurrencyLimit = System.getenv("TOKENIZE_CONCURRENCY_LIMIT"); - - Dotenv dotenv = null; - try { - dotenv = Dotenv.load(); - } catch (DotenvException ignored) {} - - if (userProvidedBatchSize == null && dotenv != null) { - userProvidedBatchSize = dotenv.get("TOKENIZE_BATCH_SIZE"); - } - if (userProvidedConcurrencyLimit == null && dotenv != null) { - userProvidedConcurrencyLimit = dotenv.get("TOKENIZE_CONCURRENCY_LIMIT"); - } + String userProvidedBatchSize = settingResolver.apply("TOKENIZE_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("TOKENIZE_CONCURRENCY_LIMIT"); if (userProvidedBatchSize != null) { try { @@ -698,22 +700,8 @@ private BatchConfig configureDetokenizeConcurrencyAndBatchSize(int totalRequests int batchSize = Constants.DETOKENIZE_BATCH_SIZE; int concurrencyLimit; try { - String userProvidedBatchSize = System.getenv("DETOKENIZE_BATCH_SIZE"); - String userProvidedConcurrencyLimit = System.getenv("DETOKENIZE_CONCURRENCY_LIMIT"); - - Dotenv dotenv = null; - try { - dotenv = Dotenv.load(); - } catch (DotenvException ignored) { - // ignore the case if .env file is not found - } - - if (userProvidedBatchSize == null && dotenv != null) { - userProvidedBatchSize = dotenv.get("DETOKENIZE_BATCH_SIZE"); - } - if (userProvidedConcurrencyLimit == null && dotenv != null) { - userProvidedConcurrencyLimit = dotenv.get("DETOKENIZE_CONCURRENCY_LIMIT"); - } + String userProvidedBatchSize = settingResolver.apply("DETOKENIZE_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("DETOKENIZE_CONCURRENCY_LIMIT"); if (userProvidedBatchSize != null) { try { @@ -877,7 +865,11 @@ private List> insertBatchFutures( RequestContext ctx = new RequestContext("INSERT"); if (interceptor != null) interceptor.intercept(ctx); CompletableFuture future = CompletableFuture - .supplyAsync(() -> insertBatch(batch, insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, ctx), executor) + .supplyAsync(() -> insertBatch( + batch, + insertRequest.getTableName().isPresent() ? insertRequest.getTableName().get() : null, + insertRequest.getUpsert().isPresent() ? insertRequest.getUpsert().get() : null, + ctx), executor) .thenApply(response -> Utils.formatBulkInsertResponse(response.body(), batchNumber, cfg.batchSize, response.headers())) .exceptionally(ex -> new BulkInsertResponse( Utils.handleBulkInsertBatchException(ex, batch, batchNumber, cfg.batchSize))); @@ -889,15 +881,20 @@ private List> insertBatchFutures( return futures; } - // Upsert is not carried at the envelope level: getInsertRequestBody pushes it down onto every - // record, so each batch already has it. - private ApiClientHttpResponse insertBatch(List batch, String tableName, RequestContext ctx) { + // tableName and upsert live on the envelope when the caller set them at the request level, and + // batching rebuilds the envelope per batch — so both have to be re-applied here or they are + // silently dropped for every batch after the body was built. + private ApiClientHttpResponse insertBatch(List batch, String tableName, + V1Upsert upsert, RequestContext ctx) { V1InsertRequest.Builder req = V1InsertRequest.builder() .vaultId(this.getVaultConfig().getVaultId()) .records(batch); if (tableName != null && !tableName.isEmpty()) { req.tableName(tableName); } + if (upsert != null) { + req.upsert(upsert); + } V1InsertRequest request = req.build(); return this.getRecordsApi().withRawResponse().insert(request, buildRequestOptions(ctx)); } @@ -906,22 +903,8 @@ private BatchConfig configureInsertConcurrencyAndBatchSize(int totalRequests) { int batchSize = Constants.INSERT_BATCH_SIZE; int concurrencyLimit; try { - String userProvidedBatchSize = System.getenv("INSERT_BATCH_SIZE"); - String userProvidedConcurrencyLimit = System.getenv("INSERT_CONCURRENCY_LIMIT"); - - Dotenv dotenv = null; - try { - dotenv = Dotenv.load(); - } catch (DotenvException ignored) { - // ignore the case if .env file is not found - } - - if (userProvidedBatchSize == null && dotenv != null) { - userProvidedBatchSize = dotenv.get("INSERT_BATCH_SIZE"); - } - if (userProvidedConcurrencyLimit == null && dotenv != null) { - userProvidedConcurrencyLimit = dotenv.get("INSERT_CONCURRENCY_LIMIT"); - } + String userProvidedBatchSize = settingResolver.apply("INSERT_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("INSERT_CONCURRENCY_LIMIT"); if (userProvidedBatchSize != null) { try { diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensOptions.java new file mode 100644 index 00000000..f8e212fd --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensOptions.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +/** + * Per-call options for bulk delete tokens. + * + *

Adds nothing to {@link DeleteTokensOptions} today; it exists so the bulk interfaces have their own + * options type to grow into, matching the {@link BulkDeleteTokensRequest} / {@link DeleteTokensRequest} split. + */ +public final class BulkDeleteTokensOptions extends DeleteTokensOptions { + + private BulkDeleteTokensOptions(BulkDeleteTokensOptionsBuilder builder) { + super(builder); + } + + public static BulkDeleteTokensOptionsBuilder builder() { + return new BulkDeleteTokensOptionsBuilder(); + } + + public static final class BulkDeleteTokensOptionsBuilder extends Builder { + + private BulkDeleteTokensOptionsBuilder() {} + + @Override + public BulkDeleteTokensOptionsBuilder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkDeleteTokensOptions build() { + return new BulkDeleteTokensOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensRequest.java index 8b8e56ea..5d123922 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensRequest.java @@ -2,33 +2,29 @@ import java.util.List; -public class BulkDeleteTokensRequest { - private final BulkDeleteTokensRequestBuilder builder; +public class BulkDeleteTokensRequest extends DeleteTokensRequest { - private BulkDeleteTokensRequest(BulkDeleteTokensRequestBuilder builder) { - this.builder = builder; + private BulkDeleteTokensRequest(List tokens) { + super(tokens); } public static BulkDeleteTokensRequestBuilder builder() { return new BulkDeleteTokensRequestBuilder(); } - public List getTokens() { - return this.builder.tokens; - } - - public static final class BulkDeleteTokensRequestBuilder { - private List tokens; + public static final class BulkDeleteTokensRequestBuilder extends DeleteTokensRequestBuilder { private BulkDeleteTokensRequestBuilder() {} + @Override public BulkDeleteTokensRequestBuilder tokens(List tokens) { this.tokens = tokens; return this; } + @Override public BulkDeleteTokensRequest build() { - return new BulkDeleteTokensRequest(this); + return new BulkDeleteTokensRequest(this.tokens); } } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponse.java index 47733c5d..d4530145 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponse.java @@ -4,6 +4,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; +import java.util.ArrayList; import java.util.List; public class BulkDeleteTokensResponse { @@ -11,40 +12,80 @@ public class BulkDeleteTokensResponse { private DeleteTokensSummary summary; @Expose(serialize = true) - private List success; - - @Expose(serialize = true) - private List errors; + private List records; private List originalPayload; + private List tokensToRetry; - public BulkDeleteTokensResponse(List success, List errors) { - this.success = success; - this.errors = errors; + public BulkDeleteTokensResponse(List records) { + this.records = records; } - public BulkDeleteTokensResponse(List success, List errors, List originalPayload) { - this.success = success; - this.errors = errors; + public BulkDeleteTokensResponse(List records, List originalPayload) { + this.records = records; this.originalPayload = originalPayload; - this.summary = new DeleteTokensSummary(this.originalPayload.size(), this.success.size(), this.errors.size()); + this.summary = buildSummary(this.records, this.originalPayload); + } + + private static DeleteTokensSummary buildSummary(List records, List originalPayload) { + int totalDeleted = 0; + int totalFailed = 0; + if (records != null) { + for (BulkDeleteTokensResponseRecord record : records) { + if (record.getError() == null) { + totalDeleted++; + } else { + totalFailed++; + } + } + } + int totalTokens = originalPayload != null ? originalPayload.size() : totalDeleted + totalFailed; + return new DeleteTokensSummary(totalTokens, totalDeleted, totalFailed); } public DeleteTokensSummary getSummary() { return summary; } - public List getSuccess() { - return success; + public List getRecords() { + return records; + } + + /** + * The tokens whose delete failed with a retryable status, ready to be resubmitted. + * + *

Retryable means a 5xx other than 529, matching the rule used by bulk insert and bulk + * detokenize. Tokens are read straight off the failed records, so a token is never dropped + * because of an index mismatch. + */ + public List getTokensToRetry() { + if (tokensToRetry == null) { + tokensToRetry = new ArrayList<>(); + if (records != null) { + for (BulkDeleteTokensResponseRecord record : records) { + if (isRetryable(record) && record.getToken() != null) { + tokensToRetry.add(record.getToken()); + } + } + } + } + return tokensToRetry; } - public List getErrors() { - return errors; + private static boolean isRetryable(BulkDeleteTokensResponseRecord record) { + Integer httpCode = record.getHttpCode(); + return record.getError() != null + && httpCode != null + && httpCode >= 500 && httpCode <= 599 + && httpCode != 529; } @Override public String toString() { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .serializeNulls() + .create(); return gson.toJson(this); } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponseRecord.java new file mode 100644 index 00000000..879733cd --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponseRecord.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +/** + * A {@link DeleteTokensRecord} carrying the token's position in the original bulk request. + */ +public class BulkDeleteTokensResponseRecord extends DeleteTokensRecord { + @Expose(serialize = true) + private final int index; + + public BulkDeleteTokensResponseRecord(int index, String token, Integer httpCode, String error) { + this(index, token, httpCode, error, null); + } + + public BulkDeleteTokensResponseRecord(int index, String token, Integer httpCode, + String error, String requestId) { + super(token, httpCode, error, requestId); + this.index = index; + } + + public int getIndex() { + return index; + } + + @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/BulkDetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java new file mode 100644 index 00000000..12d57ca5 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeOptions.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +// Bulk counterpart of DetokenizeOptions. Carries no extra state today; the interceptor +// field is inherited. +public class BulkDetokenizeOptions extends DetokenizeOptions { + + protected BulkDetokenizeOptions(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends DetokenizeOptions.Builder { + + private Builder() { + } + + @Override + public Builder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkDetokenizeOptions build() { + return new BulkDetokenizeOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java new file mode 100644 index 00000000..6dfea493 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertOptions.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +// Bulk counterpart of InsertOptions. Carries no extra state today; the interceptor +// field is inherited. +public class BulkInsertOptions extends InsertOptions { + + protected BulkInsertOptions(Builder builder) { + super(builder); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends InsertOptions.Builder { + + private Builder() { + } + + @Override + public Builder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkInsertOptions build() { + return new BulkInsertOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeOptions.java new file mode 100644 index 00000000..36d47507 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeOptions.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +/** + * Per-call options for bulk tokenize. + * + *

Adds nothing to {@link TokenizeOptions} today; it exists so the bulk interfaces have their own + * options type to grow into, matching the {@link BulkTokenizeRequest} / {@link TokenizeRequest} split. + */ +public final class BulkTokenizeOptions extends TokenizeOptions { + + private BulkTokenizeOptions(BulkTokenizeOptionsBuilder builder) { + super(builder); + } + + public static BulkTokenizeOptionsBuilder builder() { + return new BulkTokenizeOptionsBuilder(); + } + + public static final class BulkTokenizeOptionsBuilder extends Builder { + + private BulkTokenizeOptionsBuilder() {} + + @Override + public BulkTokenizeOptionsBuilder interceptor(RequestInterceptor interceptor) { + super.interceptor(interceptor); + return this; + } + + @Override + public BulkTokenizeOptions build() { + return new BulkTokenizeOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRecord.java deleted file mode 100644 index 5275e8db..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRecord.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.skyflow.vault.data; - -import java.util.List; - -public class BulkTokenizeRecord { - private final BulkTokenizeRecordBuilder builder; - - private BulkTokenizeRecord(BulkTokenizeRecordBuilder builder) { - this.builder = builder; - } - - public static BulkTokenizeRecordBuilder builder() { - return new BulkTokenizeRecordBuilder(); - } - - public Object getValue() { - return this.builder.value; - } - - public List getTokenGroupNames() { - return this.builder.tokenGroupNames; - } - - public static final class BulkTokenizeRecordBuilder { - private Object value; - private List tokenGroupNames; - - private BulkTokenizeRecordBuilder() {} - - public BulkTokenizeRecordBuilder value(Object value) { - this.value = value; - return this; - } - - public BulkTokenizeRecordBuilder tokenGroupNames(List tokenGroupNames) { - this.tokenGroupNames = tokenGroupNames; - return this; - } - - public BulkTokenizeRecord build() { - return new BulkTokenizeRecord(this); - } - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java index 47f2ef36..852f00d9 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java @@ -1,34 +1,50 @@ package com.skyflow.vault.data; import java.util.ArrayList; +import java.util.List; -public class BulkTokenizeRequest { - private final BulkTokenizeRequestBuilder builder; +public class BulkTokenizeRequest extends TokenizeRequest { - private BulkTokenizeRequest(BulkTokenizeRequestBuilder builder) { - this.builder = builder; + private BulkTokenizeRequest(List records) { + super(records); } public static BulkTokenizeRequestBuilder builder() { return new BulkTokenizeRequestBuilder(); } - public ArrayList getData() { - return this.builder.data; + /** + * Narrows the inherited accessor to the bulk record type. Safe because the builder only ever + * stores {@link BulkTokenizeRequestRecord}s. + */ + @Override + @SuppressWarnings("unchecked") + public List getRecords() { + return (List) super.getRecords(); } - public static final class BulkTokenizeRequestBuilder { - private ArrayList data; + public static final class BulkTokenizeRequestBuilder extends TokenizeRequestBuilder { private BulkTokenizeRequestBuilder() {} - public BulkTokenizeRequestBuilder data(ArrayList data) { - this.data = data; + @Override + public BulkTokenizeRequestBuilder records(List records) { + this.records = records; return this; } + @Override public BulkTokenizeRequest build() { - return new BulkTokenizeRequest(this); + List bulkRecords = null; + if (this.records != null) { + bulkRecords = new ArrayList<>(); + for (TokenizeRequestRecord record : this.records) { + // fail fast and clearly if a plain TokenizeRequestRecord was supplied to the + // bulk builder through the inherited setter + bulkRecords.add((BulkTokenizeRequestRecord) record); + } + } + return new BulkTokenizeRequest(bulkRecords); } } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequestRecord.java new file mode 100644 index 00000000..cdf792d9 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequestRecord.java @@ -0,0 +1,50 @@ +package com.skyflow.vault.data; + +import java.util.List; + +/** + * A record in a bulk tokenize request. + * + *

Carries nothing beyond {@link TokenizeRequestRecord} today. It exists as its own type so the + * bulk request has somewhere to grow, mirroring the {@link BulkInsertRecord} / {@link InsertRecord} + * split. The index that correlates a record with its result is assigned by the SDK from list + * position and appears only on {@link BulkTokenizeResponseRecord} — callers never supply it. + */ +public class BulkTokenizeRequestRecord extends TokenizeRequestRecord { + + private BulkTokenizeRequestRecord(BulkTokenizeRequestRecordBuilder builder) { + super(builder); + } + + public static BulkTokenizeRequestRecordBuilder builder() { + return new BulkTokenizeRequestRecordBuilder(); + } + + public static final class BulkTokenizeRequestRecordBuilder extends TokenizeRequestRecordBuilder { + + private BulkTokenizeRequestRecordBuilder() {} + + @Override + public BulkTokenizeRequestRecordBuilder value(Object value) { + this.value = value; + return this; + } + + @Override + public BulkTokenizeRequestRecordBuilder token(Object token) { + this.token = token; + return this; + } + + @Override + public BulkTokenizeRequestRecordBuilder tokenGroupNames(List tokenGroupNames) { + this.tokenGroupNames = tokenGroupNames; + return this; + } + + @Override + public BulkTokenizeRequestRecord build() { + return new BulkTokenizeRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java index 84026905..115fb9b3 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java @@ -4,77 +4,127 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; -import java.util.HashSet; +import java.util.ArrayList; import java.util.List; -import java.util.Set; public class BulkTokenizeResponse { @Expose(serialize = true) private TokenizeSummary summary; @Expose(serialize = true) - private List success; + private List records; - @Expose(serialize = true) - private List errors; - - private List originalPayload; + private List originalPayload; + private List recordsToRetry; - public BulkTokenizeResponse(List success, List errors) { - this.success = success; - this.errors = errors; + public BulkTokenizeResponse(List records) { + this.records = records; } - public BulkTokenizeResponse(List success, List errors, List originalPayload) { - this.success = success; - this.errors = errors; + public BulkTokenizeResponse(List records, + List originalPayload) { + this.records = records; this.originalPayload = originalPayload; + this.summary = buildSummary(this.records, this.originalPayload); + } - int totalTokens = originalPayload.size(); - - // Collect indices that appear in success and in errors - Set successIndices = new HashSet<>(); - for (TokenizeSuccess s : this.success) { - successIndices.add(s.getIndex()); - } - Set errorIndices = new HashSet<>(); - for (ErrorRecord e : this.errors) { - errorIndices.add(e.getIndex()); - } - - // totalTokenized = ALL token groups succeeded (in success, NOT in errors) - // totalPartial = SOME succeeded, SOME failed (in both success AND errors) - // totalFailed = ALL token groups failed (in errors, NOT in success) + /** + * {@code totalTokens} counts the input values submitted. The remaining three classify each + * value by how its token groups fared, so together they sum to the number of values. + */ + private static TokenizeSummary buildSummary(List records, + List originalPayload) { int totalTokenized = 0; int totalPartial = 0; int totalFailed = 0; - for (int i = 0; i < totalTokens; i++) { - boolean hasSuccess = successIndices.contains(i); - boolean hasError = errorIndices.contains(i); - if (hasSuccess && !hasError) totalTokenized++; - else if (hasSuccess && hasError) totalPartial++; - else if (!hasSuccess && hasError) totalFailed++; - // else: index not present in either (shouldn't happen in practice) + if (records != null) { + for (BulkTokenizeResponseRecord record : records) { + int succeeded = 0; + int failed = 0; + if (record.getTokens() != null) { + for (TokenizeResponseToken token : record.getTokens()) { + if (token.getError() == null) { + succeeded++; + } else { + failed++; + } + } + } + if (succeeded > 0 && failed > 0) { + totalPartial++; + } else if (succeeded > 0) { + totalTokenized++; + } else { + // no token groups came back, or every one of them failed + totalFailed++; + } + } } - - this.summary = new TokenizeSummary(totalTokens, totalTokenized, totalPartial, totalFailed); + int totalTokens = originalPayload != null + ? originalPayload.size() + : (records != null ? records.size() : 0); + return new TokenizeSummary(totalTokens, totalTokenized, totalPartial, totalFailed); } public TokenizeSummary getSummary() { return summary; } - public List getSuccess() { - return success; + public List getRecords() { + return records; + } + + /** + * The records that failed with a retryable status, ready to be resubmitted as a new bulk request. + * + *

Retryable means a 5xx other than 529, matching the rule used elsewhere in the SDK. The + * caller's original record objects are returned unchanged — they carry no index, exactly as they + * were supplied. Records where nothing failed retryably are omitted. + */ + public List getRecordsToRetry() { + if (recordsToRetry == null) { + recordsToRetry = new ArrayList<>(); + if (records != null && originalPayload != null) { + for (BulkTokenizeResponseRecord record : records) { + // the SDK assigns the index from the record's position in originalPayload, so a + // positional lookup is exact + int index = record.getIndex(); + if (index < 0 || index >= originalPayload.size() || !hasRetryableFailure(record)) { + continue; + } + recordsToRetry.add(originalPayload.get(index)); + } + } + } + return recordsToRetry; + } + + private static boolean hasRetryableFailure(BulkTokenizeResponseRecord record) { + if (record.getTokens() == null) { + return false; + } + for (TokenizeResponseToken token : record.getTokens()) { + if (isRetryable(token)) { + return true; + } + } + return false; } - public List getErrors() { - return errors; + private static boolean isRetryable(TokenizeResponseToken token) { + Integer httpCode = token.getHttpCode(); + return token.getError() != null + && httpCode != null + && httpCode >= 500 && httpCode <= 599 + && httpCode != 529; } @Override public String toString() { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + Gson gson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .serializeNulls() + .create(); return gson.toJson(this); } } diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java new file mode 100644 index 00000000..bfcae073 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponseRecord.java @@ -0,0 +1,31 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.List; + +/** + * A {@link TokenizeResponseRecord} carrying the index of the input value it belongs to. The index + * is the one supplied on the matching {@link BulkTokenizeRequestRecord}, echoed back unchanged. + */ +public class BulkTokenizeResponseRecord extends TokenizeResponseRecord { + @Expose(serialize = true) + private final int index; + + public BulkTokenizeResponseRecord(int index, Object value, List tokens) { + super(value, tokens); + this.index = index; + } + + public int getIndex() { + return index; + } + + @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/DeleteTokensOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java index 622f531e..67a3e849 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java @@ -1,9 +1,15 @@ package com.skyflow.vault.data; -public final class DeleteTokensOptions { +/** + * Per-call options for delete tokens. + * + *

Subclassed by {@link BulkDeleteTokensOptions} so the bulk interfaces can take their own options + * type while sharing this one's settings, mirroring how {@link BulkDeleteTokensRequest} extends {@link DeleteTokensRequest}. + */ +public class DeleteTokensOptions { private final RequestInterceptor interceptor; - private DeleteTokensOptions(Builder builder) { + protected DeleteTokensOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +21,11 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() {} + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRecord.java new file mode 100644 index 00000000..50f05e86 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRecord.java @@ -0,0 +1,63 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +/** + * A single token's delete outcome. {@code token} and {@code httpCode} are present on both the + * success and the error path; {@code error} is null when the token was deleted successfully. + * + *

{@code requestId} identifies the API call this outcome came from and is set only when the + * outcome is an error, since that is when it is useful for support. Bulk requests are split into + * batches, so every error from the same batch carries the same id and errors from different + * batches carry different ones. + */ +public class DeleteTokensRecord { + @Expose(serialize = true) + private final String token; + + @Expose(serialize = true) + private final Integer httpCode; + + @Expose(serialize = true) + private final String error; + + @Expose(serialize = true) + private final String requestId; + + public DeleteTokensRecord(String token, Integer httpCode, String error) { + this(token, httpCode, error, null); + } + + public DeleteTokensRecord(String token, Integer httpCode, String error, String requestId) { + this.token = token; + this.httpCode = httpCode; + this.error = error; + // a successful delete carries no request id, whatever the caller passed + this.requestId = error != null ? requestId : null; + } + + public String getToken() { + return token; + } + + public Integer getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java new file mode 100644 index 00000000..98ab8922 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensRequest.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class DeleteTokensRequest { + private final List tokens; + + protected DeleteTokensRequest(List tokens) { + this.tokens = tokens; + } + + public static DeleteTokensRequestBuilder builder() { + return new DeleteTokensRequestBuilder(); + } + + public List getTokens() { + return this.tokens; + } + + public static class DeleteTokensRequestBuilder { + protected List tokens; + + protected DeleteTokensRequestBuilder() {} + + public DeleteTokensRequestBuilder tokens(List tokens) { + this.tokens = tokens; + return this; + } + + public DeleteTokensRequest build() { + return new DeleteTokensRequest(this.tokens); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java new file mode 100644 index 00000000..46c3887a --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensResponse.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class DeleteTokensResponse { + private final List records; + + public DeleteTokensResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensSuccess.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensSuccess.java deleted file mode 100644 index 15da5a46..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensSuccess.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.annotations.Expose; - -public class DeleteTokensSuccess { - @Expose(serialize = true) - private int index; - - @Expose(serialize = true) - private String token; - - public DeleteTokensSuccess(int index, String token) { - this.index = index; - this.token = token; - } - - public int getIndex() { - return index; - } - - public String getToken() { - return token; - } - - @Override - public String toString() { - Gson gson = new Gson(); - return gson.toJson(this); - } -} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java index 267bd9bd..ddc18bdf 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java @@ -1,9 +1,9 @@ package com.skyflow.vault.data; -public final class DetokenizeOptions { +public class DetokenizeOptions { private final RequestInterceptor interceptor; - private DetokenizeOptions(Builder builder) { + protected DetokenizeOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +15,12 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() { + } + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java index 73262b98..a3d5aa79 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java @@ -1,9 +1,9 @@ package com.skyflow.vault.data; -public final class InsertOptions { +public class InsertOptions { private final RequestInterceptor interceptor; - private InsertOptions(Builder builder) { + protected InsertOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +15,12 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() { + } + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java index 2ac5bcfd..f955a731 100644 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java @@ -1,9 +1,15 @@ package com.skyflow.vault.data; -public final class TokenizeOptions { +/** + * Per-call options for tokenize. + * + *

Subclassed by {@link BulkTokenizeOptions} so the bulk interfaces can take their own options + * type while sharing this one's settings, mirroring how {@link BulkTokenizeRequest} extends {@link TokenizeRequest}. + */ +public class TokenizeOptions { private final RequestInterceptor interceptor; - private TokenizeOptions(Builder builder) { + protected TokenizeOptions(Builder builder) { this.interceptor = builder.interceptor; } @@ -15,9 +21,11 @@ public static Builder builder() { return new Builder(); } - public static final class Builder { + public static class Builder { private RequestInterceptor interceptor; + protected Builder() {} + public Builder interceptor(RequestInterceptor interceptor) { this.interceptor = interceptor; return this; diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java new file mode 100644 index 00000000..e08a8f19 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequest.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class TokenizeRequest { + private final List records; + + protected TokenizeRequest(List records) { + this.records = records; + } + + public static TokenizeRequestBuilder builder() { + return new TokenizeRequestBuilder(); + } + + public List getRecords() { + return this.records; + } + + public static class TokenizeRequestBuilder { + protected List records; + + protected TokenizeRequestBuilder() {} + + public TokenizeRequestBuilder records(List records) { + this.records = records; + return this; + } + + public TokenizeRequest build() { + return new TokenizeRequest(this.records); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequestRecord.java new file mode 100644 index 00000000..3e9094e8 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeRequestRecord.java @@ -0,0 +1,59 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class TokenizeRequestRecord { + private final Object value; + private final Object token; + private final List tokenGroupNames; + + protected TokenizeRequestRecord(TokenizeRequestRecordBuilder builder) { + this.value = builder.value; + this.token = builder.token; + this.tokenGroupNames = builder.tokenGroupNames; + } + + public static TokenizeRequestRecordBuilder builder() { + return new TokenizeRequestRecordBuilder(); + } + + public Object getValue() { + return this.value; + } + + /** Bring-your-own-token value, when the caller supplies the token instead of generating one. */ + public Object getToken() { + return this.token; + } + + public List getTokenGroupNames() { + return this.tokenGroupNames; + } + + public static class TokenizeRequestRecordBuilder { + protected Object value; + protected Object token; + protected List tokenGroupNames; + + protected TokenizeRequestRecordBuilder() {} + + public TokenizeRequestRecordBuilder value(Object value) { + this.value = value; + return this; + } + + public TokenizeRequestRecordBuilder token(Object token) { + this.token = token; + return this; + } + + public TokenizeRequestRecordBuilder tokenGroupNames(List tokenGroupNames) { + this.tokenGroupNames = tokenGroupNames; + return this; + } + + public TokenizeRequestRecord build() { + return new TokenizeRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java new file mode 100644 index 00000000..d27f66d3 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponse.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +public class TokenizeResponse { + private final List response; + + public TokenizeResponse(List response) { + this.response = response; + } + + public List getResponse() { + return response; + } + + @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/TokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java new file mode 100644 index 00000000..84da4cef --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseRecord.java @@ -0,0 +1,38 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.List; + +/** + * The tokenization outcome for one input value: every requested token group is reported in + * {@code tokens}, whether it succeeded or failed. + */ +public class TokenizeResponseRecord { + @Expose(serialize = true) + private final Object value; + + @Expose(serialize = true) + private final List tokens; + + public TokenizeResponseRecord(Object value, List tokens) { + this.value = value; + this.tokens = tokens; + } + + public Object getValue() { + return value; + } + + public List getTokens() { + return tokens; + } + + @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/TokenizeResponseToken.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java new file mode 100644 index 00000000..dff54b77 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeResponseToken.java @@ -0,0 +1,72 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +/** + * One token-group outcome for a single input value. {@code token} is populated on success and + * {@code error} on failure; {@code httpCode} is present on both paths. + * + *

{@code requestId} identifies the API call this outcome came from and is set only when the + * outcome is an error, since that is when it is useful for support. Bulk requests are split into + * batches, so every error from the same batch carries the same id and errors from different + * batches carry different ones. + */ +public class TokenizeResponseToken { + @Expose(serialize = true) + private final String tokenGroupName; + + @Expose(serialize = true) + private final String token; + + @Expose(serialize = true) + private final Integer httpCode; + + @Expose(serialize = true) + private final String error; + + @Expose(serialize = true) + private final String requestId; + + public TokenizeResponseToken(String tokenGroupName, String token, Integer httpCode, String error) { + this(tokenGroupName, token, httpCode, error, null); + } + + public TokenizeResponseToken(String tokenGroupName, String token, Integer httpCode, + String error, String requestId) { + this.tokenGroupName = tokenGroupName; + this.token = token; + this.httpCode = httpCode; + this.error = error; + // a successful outcome carries no request id, whatever the caller passed + this.requestId = error != null ? requestId : null; + } + + public String getTokenGroupName() { + return tokenGroupName; + } + + public String getToken() { + return token; + } + + public Integer getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } + + /** The API call this outcome came from; null unless this is an error. */ + public String getRequestId() { + return requestId; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeSuccess.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeSuccess.java deleted file mode 100644 index 59ee4b65..00000000 --- a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeSuccess.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.annotations.Expose; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Represents one successfully tokenized input record. - * All tokens generated for this record (one per tokenGroupName) are grouped - * under the {@code tokens} map: tokenGroupName → token value. - */ -public class TokenizeSuccess { - @Expose(serialize = true) - private int index; - - @Expose(serialize = true) - private Object value; - - @Expose(serialize = true) - private Map tokens; - - public TokenizeSuccess(int index, Object value) { - this.index = index; - this.value = value; - this.tokens = new LinkedHashMap<>(); - } - - /** Adds a token for a specific token group name. */ - public void addToken(String tokenGroupName, String token) { - this.tokens.put(tokenGroupName, token); - } - - public int getIndex() { - return index; - } - - public Object getValue() { - return value; - } - - public Map getTokens() { - return tokens; - } - - @Override - public String toString() { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - return gson.toJson(this); - } -} diff --git a/flowvault/src/test/java/com/skyflow/AuthInterceptorTests.java b/flowvault/src/test/java/com/skyflow/AuthInterceptorTests.java new file mode 100644 index 00000000..a0f91dbb --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/AuthInterceptorTests.java @@ -0,0 +1,115 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.core.RetryInterceptor; +import com.skyflow.utils.FakeChain; +import com.skyflow.utils.SkyflowRetryInterceptor; +import okhttp3.Interceptor; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +/** + * The auth interceptor installed by {@link VaultClient#updateExecutorInHTTP()}. + * + *

It is a lambda, so the only way to reach it is to pull it back off the built OkHttpClient and + * drive it with a fake chain. The property that matters is that it reads {@code this.token} on every + * request rather than capturing it once — that is why retry is registered outside auth, so a replayed + * attempt picks up a refreshed token instead of resending an expired one. + */ +public class AuthInterceptorTests { + + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; + + private static VaultConfig config() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault1"); + config.setClusterId("cluster1"); + config.setEnv(Env.DEV); + return config; + } + + private static Interceptor authInterceptorOf(VaultClient client) { + client.updateExecutorInHTTP(); + List interceptors = client.sharedHttpClient.interceptors(); + for (Interceptor interceptor : interceptors) { + if (!(interceptor instanceof SkyflowRetryInterceptor) && !(interceptor instanceof RetryInterceptor)) { + return interceptor; + } + } + throw new AssertionError("no auth interceptor installed"); + } + + @Test + public void testAuthInterceptor_addsBearerAuthorizationHeader() throws SkyflowException, IOException { + Credentials credentials = new Credentials(); + credentials.setApiKey(API_KEY); + VaultConfig config = config(); + config.setCredentials(credentials); + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + + FakeChain chain = new FakeChain(200); + authInterceptorOf(client).intercept(chain); + + Assert.assertEquals("Bearer " + API_KEY, chain.lastProceeded().header("Authorization")); + } + + @Test + public void testAuthInterceptor_readsTheTokenFreshOnEveryRequest() throws SkyflowException, IOException { + VaultClient client = new VaultClient(config(), null); + Interceptor auth = authInterceptorOf(client); + + client.token = "first-token"; + FakeChain first = new FakeChain(200); + auth.intercept(first); + + client.token = "second-token"; + FakeChain second = new FakeChain(200); + auth.intercept(second); + + Assert.assertEquals("Bearer first-token", first.lastProceeded().header("Authorization")); + Assert.assertEquals("A captured token would resend the stale value after a refresh", + "Bearer second-token", second.lastProceeded().header("Authorization")); + } + + @Test + public void testAuthInterceptor_leavesTheRestOfTheRequestAlone() throws SkyflowException, IOException { + VaultClient client = new VaultClient(config(), null); + client.token = "t"; + FakeChain chain = new FakeChain(200); + + authInterceptorOf(client).intercept(chain); + + Assert.assertEquals(chain.request().url(), chain.lastProceeded().url()); + Assert.assertEquals(chain.request().method(), chain.lastProceeded().method()); + } + + @Test + public void testAuthInterceptor_returnsTheChainResponseUntouched() throws SkyflowException, IOException { + VaultClient client = new VaultClient(config(), null); + client.token = "t"; + FakeChain chain = new FakeChain(503); + + Assert.assertEquals(503, authInterceptorOf(client).intercept(chain).code()); + Assert.assertEquals("auth must not retry - that is the outer interceptor's job", 1, chain.calls()); + } + + @Test + public void testGetQueryApi_availableAfterSetBearerToken() throws SkyflowException { + Credentials credentials = new Credentials(); + credentials.setApiKey(API_KEY); + VaultConfig config = config(); + config.setCredentials(credentials); + + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + + Assert.assertNotNull(client.getQueryApi()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/HttpConfigTests.java b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java index 49ce259c..3e0e4f7f 100644 --- a/flowvault/src/test/java/com/skyflow/HttpConfigTests.java +++ b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java @@ -3,14 +3,13 @@ import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.errors.SkyflowException; -import com.skyflow.generated.rest.core.RetryInterceptor; +import com.skyflow.utils.SkyflowRetryInterceptor; import com.skyflow.vault.controller.VaultController; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import org.junit.Assert; import org.junit.Test; -import java.lang.reflect.Field; import java.util.List; /** @@ -37,30 +36,17 @@ private static OkHttpClient httpClientOf(VaultClient client) { return client.sharedHttpClient; } - /** - * Reads the retry budget back out of the generated RetryInterceptor, which exposes no getter. - * If regeneration changes its internals this fails loudly — which is the point. - */ - private static int maxRetriesOf(RetryInterceptor interceptor) { - try { - Field backoffField = RetryInterceptor.class.getDeclaredField("backoff"); - backoffField.setAccessible(true); - Object backoff = backoffField.get(interceptor); - Field maxField = backoff.getClass().getDeclaredField("maxNumRetries"); - maxField.setAccessible(true); - return (int) maxField.get(backoff); - } catch (ReflectiveOperationException e) { - throw new AssertionError("RetryInterceptor internals changed: " + e.getMessage(), e); - } + private static int maxRetriesOf(SkyflowRetryInterceptor interceptor) { + return interceptor.getMaxRetries(); } - private static RetryInterceptor retryInterceptorOf(OkHttpClient http) { + private static SkyflowRetryInterceptor retryInterceptorOf(OkHttpClient http) { for (Interceptor interceptor : http.interceptors()) { - if (interceptor instanceof RetryInterceptor) { - return (RetryInterceptor) interceptor; + if (interceptor instanceof SkyflowRetryInterceptor) { + return (SkyflowRetryInterceptor) interceptor; } } - throw new AssertionError("No RetryInterceptor installed on the HTTP client"); + throw new AssertionError("No SkyflowRetryInterceptor installed on the HTTP client"); } // ── SDK defaults (neither level configured) ─────────────────────────────── @@ -93,7 +79,7 @@ public void testDefaults_perAttemptTimeoutsLeftAtOkHttpDefaults() throws Skyflow @Test public void testClientWideConfig_appliesWhenVaultLevelUnset() throws SkyflowException { VaultClient client = new VaultClient(buildConfig(), null); - client.setCommonHttpConfig(30, 5, 6, 7, 2); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); OkHttpClient http = httpClientOf(client); @@ -136,7 +122,7 @@ public void testPrecedence_vaultLevelBeatsClientWideForEverySetting() throws Sky config.setMaxRetries(4); VaultClient client = new VaultClient(config, null); - client.setCommonHttpConfig(30, 5, 6, 7, 2); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); OkHttpClient http = httpClientOf(client); @@ -154,7 +140,7 @@ public void testPrecedence_settingsResolveIndependently() throws SkyflowExceptio config.setReadTimeout(12); VaultClient client = new VaultClient(config, null); - client.setCommonHttpConfig(30, 5, 6, 7, 2); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); OkHttpClient http = httpClientOf(client); @@ -168,7 +154,7 @@ public void testPrecedence_settingsResolveIndependently() throws SkyflowExceptio @Test public void testPrecedence_sdkDefaultUsedWhenBothLevelsNull() throws SkyflowException { VaultClient client = new VaultClient(buildConfig(), null); - client.setCommonHttpConfig(null, null, null, null, null); + client.setCommonHttpConfig(null, null, null, null, null, null, null); OkHttpClient http = httpClientOf(client); @@ -185,7 +171,7 @@ public void testSetCommonHttpConfig_rebuildsHttpClientWithNewValues() throws Sky OkHttpClient first = httpClientOf(client); Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, first.callTimeoutMillis()); - client.setCommonHttpConfig(30, null, null, null, 2); + client.setCommonHttpConfig(30, null, null, null, 2, null, null); OkHttpClient second = httpClientOf(client); Assert.assertNotSame(first, second); @@ -212,8 +198,8 @@ public void testInterceptors_retryIsOuterSoEachAttemptRereadsTheToken() throws S List interceptors = http.interceptors(); Assert.assertEquals(2, interceptors.size()); Assert.assertTrue("Retry must be registered first so it wraps the auth interceptor", - interceptors.get(0) instanceof RetryInterceptor); - Assert.assertFalse(interceptors.get(1) instanceof RetryInterceptor); + interceptors.get(0) instanceof SkyflowRetryInterceptor); + Assert.assertFalse(interceptors.get(1) instanceof SkyflowRetryInterceptor); } @Test @@ -233,7 +219,7 @@ public void testExplicitZeroTimeout_isAValueNotAnInheritSignal() throws SkyflowE config.setTimeout(0); VaultClient client = new VaultClient(config, null); - client.setCommonHttpConfig(30, null, null, null, null); + client.setCommonHttpConfig(30, null, null, null, null, null, null); Assert.assertEquals(0, httpClientOf(client).callTimeoutMillis()); } @@ -244,11 +230,114 @@ public void testExplicitZeroMaxRetries_overridesClientWideRetries() throws Skyfl config.setMaxRetries(0); VaultClient client = new VaultClient(config, null); - client.setCommonHttpConfig(null, null, null, null, 5); + client.setCommonHttpConfig(null, null, null, null, 5, null, null); Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(httpClientOf(client)))); } + // ── Retry delays ────────────────────────────────────────────────────────── + + @Test + public void testDefaults_retryDelaysAre500And2000Millis() throws SkyflowException { + SkyflowRetryInterceptor retry = retryInterceptorOf(httpClientOf(new VaultClient(buildConfig(), null))); + + Assert.assertEquals(500L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(2000L, retry.getMaxRetryDelayMillis()); + } + + @Test + public void testRetryDelays_clientWideValuesApply() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + client.setCommonHttpConfig(null, null, null, null, 3, 100L, 900L); + + SkyflowRetryInterceptor retry = retryInterceptorOf(httpClientOf(client)); + + Assert.assertEquals(100L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(900L, retry.getMaxRetryDelayMillis()); + } + + @Test + public void testRetryDelays_vaultLevelBeatsClientWide() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setInitialRetryDelayMillis(250L); + config.setMaxRetryDelayMillis(4000L); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(null, null, null, null, 3, 100L, 900L); + + SkyflowRetryInterceptor retry = retryInterceptorOf(httpClientOf(client)); + + Assert.assertEquals(250L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(4000L, retry.getMaxRetryDelayMillis()); + } + + @Test + public void testRetryDelays_resolveIndependentlyOfEachOther() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setMaxRetryDelayMillis(4000L); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(null, null, null, null, 3, 100L, 900L); + + SkyflowRetryInterceptor retry = retryInterceptorOf(httpClientOf(client)); + + Assert.assertEquals(100L, retry.getInitialRetryDelayMillis()); // client-wide + Assert.assertEquals(4000L, retry.getMaxRetryDelayMillis()); // vault + } + + @Test + public void testRetryDelays_endToEndThroughTheBuilder() throws SkyflowException { + Skyflow client = Skyflow.builder() + .maxRetries(3) + .initialRetryDelayMillis(100) + .maxRetryDelayMillis(900) + .addVaultConfig(buildConfig()) + .build(); + + SkyflowRetryInterceptor retry = retryInterceptorOf(httpClientOf(client.vault())); + + Assert.assertEquals(3, retry.getMaxRetries()); + Assert.assertEquals(100L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(900L, retry.getMaxRetryDelayMillis()); + } + + @Test + public void testRetryDelays_vaultConfigBeatsBuilderEndToEnd() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setInitialRetryDelayMillis(250L); + + Skyflow client = Skyflow.builder() + .initialRetryDelayMillis(100) + .addVaultConfig(config) + .build(); + + Assert.assertEquals(250L, + retryInterceptorOf(httpClientOf(client.vault())).getInitialRetryDelayMillis()); + } + + @Test + public void testRetryDelays_survivedUpdateVaultConfig() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig()); + + VaultConfig update = buildConfig(); + update.setInitialRetryDelayMillis(250L); + update.setMaxRetryDelayMillis(4000L); + + SkyflowRetryInterceptor retry = + retryInterceptorOf(httpClientOf(builder.updateVaultConfig(update).build().vault())); + + Assert.assertEquals(250L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(4000L, retry.getMaxRetryDelayMillis()); + } + + @Test + public void testRetryDelays_builderMethodsAreFluent() { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder(); + + Assert.assertSame(builder, builder.initialRetryDelayMillis(100)); + Assert.assertSame(builder, builder.maxRetryDelayMillis(900)); + } + // ── VaultConfig accessors ───────────────────────────────────────────────── @Test diff --git a/flowvault/src/test/java/com/skyflow/utils/FakeChain.java b/flowvault/src/test/java/com/skyflow/utils/FakeChain.java new file mode 100644 index 00000000..97012f13 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/FakeChain.java @@ -0,0 +1,136 @@ +package com.skyflow.utils; + +import okhttp3.Call; +import okhttp3.Connection; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Minimal Interceptor.Chain that replays a scripted list of status codes, so the retry loop can + * be driven without a network or a mock-server dependency. Records how many times it was called + * and which responses were closed. + */ +public final class FakeChain implements Interceptor.Chain { + + private final Request request = + new Request.Builder().url("https://cluster1.example.com/v1/flows").build(); + private final int[] statusCodes; + private final List bodies = new ArrayList<>(); + private int calls; + private Request lastProceeded; + + public FakeChain(int... statusCodes) { + this.statusCodes = statusCodes; + } + + public int calls() { + return calls; + } + + /** Responses the interceptor superseded and should have closed. */ + public List bodies() { + return bodies; + } + + /** The request as it reached the next interceptor, i.e. after any rewriting. */ + public Request lastProceeded() { + return lastProceeded; + } + + @Override + public Request request() { + return request; + } + + @Override + public Response proceed(Request request) throws IOException { + this.lastProceeded = request; + // Past the end of the script, keep returning the last code. + int code = statusCodes[Math.min(calls, statusCodes.length - 1)]; + calls++; + TrackingBody body = new TrackingBody(); + bodies.add(body); + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message("status " + code) + .body(body) + .build(); + } + + @Override + public Connection connection() { + return null; + } + + @Override + public Call call() { + throw new UnsupportedOperationException(); + } + + @Override + public int connectTimeoutMillis() { + return 0; + } + + @Override + public Interceptor.Chain withConnectTimeout(int timeout, TimeUnit unit) { + return this; + } + + @Override + public int readTimeoutMillis() { + return 0; + } + + @Override + public Interceptor.Chain withReadTimeout(int timeout, TimeUnit unit) { + return this; + } + + @Override + public int writeTimeoutMillis() { + return 0; + } + + @Override + public Interceptor.Chain withWriteTimeout(int timeout, TimeUnit unit) { + return this; + } + + public static final class TrackingBody extends ResponseBody { + boolean closed; + + @Override + public MediaType contentType() { + return null; + } + + @Override + public long contentLength() { + return 0; + } + + @Override + public BufferedSource source() { + return new Buffer(); + } + + @Override + public void close() { + closed = true; + } + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java new file mode 100644 index 00000000..43d51408 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/FlatTokenizeResponseTests.java @@ -0,0 +1,383 @@ +package com.skyflow.utils; + +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.TokenizeResponseToken; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The API returns one flat row per (value, token group) instead of the nested {@code tokens} array + * the generated wire type models, and a record rejected outright yields a single row regardless of + * how many groups it asked for. These cover folding those rows back onto the records that produced + * them. + */ +public class FlatTokenizeResponseTests { + + private static V1FlowTokenizeResponse parse(String json) { + try { + return ObjectMappers.JSON_MAPPER.readValue(json, V1FlowTokenizeResponse.class); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static BulkTokenizeRequestRecord record(Object value, String... groups) { + return BulkTokenizeRequestRecord.builder() + .value(value).tokenGroupNames(Arrays.asList(groups)).build(); + } + + private static BulkTokenizeRequestRecord byotRecord(Object value, String token, String... groups) { + return BulkTokenizeRequestRecord.builder() + .value(value).token(token).tokenGroupNames(Arrays.asList(groups)).build(); + } + + // ── the exact payload observed against the live API ──────────────────────── + + private static final String LIVE_RESPONSE = "{\n" + + " \"response\": [\n" + + " { \"token\": \"\", \"value\": \"byot-input-value\", \"tokenGroupName\": null,\n" + + " \"error\": \"Invalid request. BYOT token should contain one token group.\", \"httpCode\": 400 },\n" + + " { \"token\": \"cc1179a3-e2be-404e-9a31-4f97f27bf406\", \"value\": {\"age\": 28, \"email\": \"ka@yahoo.com\"},\n" + + " \"tokenGroupName\": \"deterministic_string_tg\", \"error\": null, \"httpCode\": 200 },\n" + + " { \"token\": \"\", \"value\": {\"age\": 28, \"email\": \"ka@yahoo.com\"}, \"tokenGroupName\": null,\n" + + " \"error\": \"Tokenize failed. Token group emailTokenGroup is invalid. Specify a valid token group.\",\n" + + " \"httpCode\": 400 }\n" + + " ]\n" + + "}"; + + @Test + public void testLiveResponse_threeRowsFoldOntoTwoRecords() { + Map objectValue = new LinkedHashMap<>(); + objectValue.put("email", "ka@yahoo.com"); + objectValue.put("age", 28); + List sent = Arrays.asList( + byotRecord("byot-input-value", "550e8400-e29b-41d4-a716-446655440000", + "deterministic_string_tg", "non_deterministic"), + record(objectValue, "deterministic_string_tg", "emailTokenGroup")); + + BulkTokenizeResponse result = + Utils.formatBulkTokenizeResponse(parse(LIVE_RESPONSE), sent, 0, new HashMap<>()); + + // two inputs in, two records out - not three + Assert.assertEquals(2, result.getRecords().size()); + + BulkTokenizeResponseRecord byot = result.getRecords().get(0); + Assert.assertEquals(0, byot.getIndex()); + Assert.assertEquals("byot-input-value", byot.getValue()); + Assert.assertEquals(1, byot.getTokens().size()); + Assert.assertEquals("Invalid request. BYOT token should contain one token group.", + byot.getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), byot.getTokens().get(0).getHttpCode()); + // the API sends "" for a token that does not apply + Assert.assertNull(byot.getTokens().get(0).getToken()); + + BulkTokenizeResponseRecord object = result.getRecords().get(1); + Assert.assertEquals(1, object.getIndex()); + Assert.assertEquals(2, object.getTokens().size()); + Assert.assertEquals("deterministic_string_tg", object.getTokens().get(0).getTokenGroupName()); + Assert.assertEquals("cc1179a3-e2be-404e-9a31-4f97f27bf406", object.getTokens().get(0).getToken()); + Assert.assertNull(object.getTokens().get(0).getError()); + Assert.assertEquals("Tokenize failed. Token group emailTokenGroup is invalid. Specify a valid token group.", + object.getTokens().get(1).getError()); + } + + @Test + public void testLiveResponse_summaryClassifiesByRecordNotByRow() { + Map objectValue = new LinkedHashMap<>(); + objectValue.put("email", "ka@yahoo.com"); + objectValue.put("age", 28); + List sent = Arrays.asList( + byotRecord("byot-input-value", "550e8400", "deterministic_string_tg", "non_deterministic"), + record(objectValue, "deterministic_string_tg", "emailTokenGroup")); + + BulkTokenizeResponse formatted = + Utils.formatBulkTokenizeResponse(parse(LIVE_RESPONSE), sent, 0, new HashMap<>()); + BulkTokenizeResponse withPayload = + new BulkTokenizeResponse(formatted.getRecords(), sent); + + // 2 values in: one wholly failed, one partially tokenized - and the counts sum to 2 + Assert.assertEquals(2, withPayload.getSummary().getTotalTokens()); + Assert.assertEquals(0, withPayload.getSummary().getTotalTokenized()); + Assert.assertEquals(1, withPayload.getSummary().getTotalPartial()); + Assert.assertEquals(1, withPayload.getSummary().getTotalFailed()); + } + + // ── duplicate token groups within one record ─────────────────────────────── + + @Test + public void testDuplicateTokenGroupsInOneRecord_bothRowsKeptUnderOneRecord() { + String json = "{\"response\": [" + + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," + + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-b\", \"httpCode\": 200}" + + "]}"; + List sent = Collections.singletonList(record("v1", "g1", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + // one record asked for the same group twice; both results stay on it rather than + // collapsing or spilling into a phantom second record + Assert.assertEquals(1, result.getRecords().size()); + List tokens = result.getRecords().get(0).getTokens(); + Assert.assertEquals(2, tokens.size()); + Assert.assertEquals("tok-a", tokens.get(0).getToken()); + Assert.assertEquals("tok-b", tokens.get(1).getToken()); + } + + // ── boundaries ───────────────────────────────────────────────────────────── + + @Test + public void testRecordRejectedOutright_nextRecordStillGetsItsOwnRows() { + // record 0 asked for 2 groups but was rejected wholesale, yielding 1 row + String json = "{\"response\": [" + + "{\"value\": \"v0\", \"tokenGroupName\": null, \"token\": \"\", \"error\": \"rejected\", \"httpCode\": 400}," + + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-1\", \"httpCode\": 200}," + + "{\"value\": \"v1\", \"tokenGroupName\": \"g2\", \"token\": \"tok-2\", \"httpCode\": 200}" + + "]}"; + List sent = Arrays.asList( + record("v0", "g1", "g2"), record("v1", "g1", "g2")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + Assert.assertEquals(2, result.getRecords().size()); + Assert.assertEquals(1, result.getRecords().get(0).getTokens().size()); + Assert.assertEquals("rejected", result.getRecords().get(0).getTokens().get(0).getError()); + Assert.assertEquals(2, result.getRecords().get(1).getTokens().size()); + Assert.assertEquals("tok-1", result.getRecords().get(1).getTokens().get(0).getToken()); + } + + @Test + public void testIndexIsOffsetByBatchStart() { + String json = "{\"response\": [" + + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-0\", \"httpCode\": 200}," + + "{\"value\": \"v1\", \"tokenGroupName\": \"g1\", \"token\": \"tok-1\", \"httpCode\": 200}" + + "]}"; + List sent = Arrays.asList(record("v0", "g1"), record("v1", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 25, new HashMap<>()); + + Assert.assertEquals(25, result.getRecords().get(0).getIndex()); + Assert.assertEquals(26, result.getRecords().get(1).getIndex()); + } + + @Test + public void testRecordTheResponseNeverMentions_stillReportedWithNoTokens() { + String json = "{\"response\": [" + + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-0\", \"httpCode\": 200}" + + "]}"; + List sent = Arrays.asList(record("v0", "g1"), record("v1", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + // the caller must still see a record per input, so indexes stay aligned with their list + Assert.assertEquals(2, result.getRecords().size()); + Assert.assertEquals(1, result.getRecords().get(1).getIndex()); + Assert.assertTrue(result.getRecords().get(1).getTokens().isEmpty()); + } + + @Test + public void testMoreRowsThanTheRequestExplains_rowsAreKeptNotDropped() { + String json = "{\"response\": [" + + "{\"value\": \"v0\", \"tokenGroupName\": \"g1\", \"token\": \"tok-0\", \"httpCode\": 200}," + + "{\"value\": \"stray\", \"tokenGroupName\": \"g9\", \"token\": \"tok-9\", \"httpCode\": 200}" + + "]}"; + List sent = Collections.singletonList(record("v0", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + Assert.assertEquals(2, result.getRecords().size()); + Assert.assertEquals("tok-9", result.getRecords().get(1).getTokens().get(0).getToken()); + } + + @Test + public void testGroupedResponseShape_stillFoldsOneRowPerRecord() { + // if the API ever returns the nested shape the wire type models, nothing changes + String json = "{\"response\": [" + + "{\"value\": \"v0\", \"tokens\": [" + + " {\"tokenGroupName\": \"g1\", \"token\": \"tok-a\", \"httpCode\": 200}," + + " {\"tokenGroupName\": \"g2\", \"token\": \"tok-b\", \"httpCode\": 200}]}," + + "{\"value\": \"v1\", \"tokens\": [" + + " {\"tokenGroupName\": \"g1\", \"token\": \"tok-c\", \"httpCode\": 200}]}" + + "]}"; + List sent = Arrays.asList( + record("v0", "g1", "g2"), record("v1", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(parse(json), sent, 0, new HashMap<>()); + + Assert.assertEquals(2, result.getRecords().size()); + Assert.assertEquals(2, result.getRecords().get(0).getTokens().size()); + Assert.assertEquals("tok-c", result.getRecords().get(1).getTokens().get(0).getToken()); + } + + // ── rejected requests still describe each record in the body ─────────────── + + /** Builds the wrapper the SDK sees: CompletableFuture wraps the cause. */ + private static Throwable rejected(int status, Object body) { + return new RuntimeException(new ApiClientApiException("Error with status code " + status, status, body)); + } + + private static Map row(String value, String group, String token, String error, int httpCode) { + Map r = new LinkedHashMap<>(); + r.put("token", token); + r.put("value", value); + r.put("tokenGroupName", group); + r.put("error", error); + r.put("httpCode", httpCode); + return r; + } + + private static Map body(Map... rows) { + Map b = new LinkedHashMap<>(); + b.put("response", Arrays.asList(rows)); + return b; + } + + @Test + public void testRejectedRequest_reportsTheApiMessageNotJustTheStatus() { + // BYOT naming two groups: the API rejects the whole record and returns ONE row for it + List sent = Collections.singletonList( + byotRecord("grace@example.com", "tok-1", "deterministic_string_tg", "non_deterministic")); + Throwable ex = rejected(400, body(row("grace@example.com", null, "", + "Invalid request. BYOT token should contain one token group.", 400))); + + List records = + Utils.handleBulkTokenizeBatchException(ex, sent, 0); + + Assert.assertEquals(1, records.size()); + // one entry, matching the API - not one fabricated per requested token group + Assert.assertEquals(1, records.get(0).getTokens().size()); + Assert.assertEquals("Invalid request. BYOT token should contain one token group.", + records.get(0).getTokens().get(0).getError()); + Assert.assertNull(records.get(0).getTokens().get(0).getTokenGroupName()); + Assert.assertEquals(Integer.valueOf(400), records.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testRejectedRequest_keepsPerRecordMessagesAndIndexes() { + List sent = Arrays.asList( + record("dave@example.com", "bad_group"), record("erin@example.com", "bad_group")); + String message = "Tokenize failed. Token group bad_group is invalid. Specify a valid token group."; + Throwable ex = rejected(400, body( + row("dave@example.com", null, "", message, 400), + row("erin@example.com", null, "", message, 400))); + + List records = + Utils.handleBulkTokenizeBatchException(ex, sent, 20); + + Assert.assertEquals(2, records.size()); + Assert.assertEquals(20, records.get(0).getIndex()); + Assert.assertEquals(21, records.get(1).getIndex()); + Assert.assertEquals(message, records.get(0).getTokens().get(0).getError()); + Assert.assertEquals(message, records.get(1).getTokens().get(0).getError()); + } + + @Test + public void testRejectedRequest_withNoUsableBodyFallsBackToTheStatusCode() { + // a transport-level failure carries no response array to read + List sent = Collections.singletonList(record("v1", "g1", "g2")); + Throwable ex = new RuntimeException("connection reset"); + + List records = + Utils.handleBulkTokenizeBatchException(ex, sent, 0); + + Assert.assertEquals(1, records.size()); + // no rows to go on, so every requested group is reported as failed + Assert.assertEquals(2, records.get(0).getTokens().size()); + Assert.assertEquals("connection reset", records.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), records.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testRejectedRequest_withUnfamiliarBodyFallsBackToTheStatusCode() { + List sent = Collections.singletonList(record("v1", "g1")); + Map opaque = new LinkedHashMap<>(); + opaque.put("error", "gateway timeout"); + + List records = + Utils.handleBulkTokenizeBatchException(rejected(504, opaque), sent, 0); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("gateway timeout", records.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(504), records.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testRejectedRequest_retryableStatusStillSurfacesForRetry() { + List sent = Collections.singletonList(record("v1", "g1")); + Throwable ex = rejected(503, body(row("v1", "g1", "", "service unavailable", 503))); + + List records = + Utils.handleBulkTokenizeBatchException(ex, sent, 0); + BulkTokenizeResponse response = new BulkTokenizeResponse(records, sent); + + Assert.assertEquals(1, response.getRecordsToRetry().size()); + Assert.assertSame(sent.get(0), response.getRecordsToRetry().get(0)); + } + + // ── duplicate-value batching ─────────────────────────────────────────────── + + @Test + public void testBatching_cutsBatchShortWhenAValueRepeats() { + List records = Arrays.asList( + record("alice", "g1"), record("bob", "g1"), record("alice", "g1"), record("carol", "g1")); + + List> batches = Utils.createBulkTokenizeBatches(records, 10); + + // the repeat of "alice" must not share a request with the first one + Assert.assertEquals(2, batches.size()); + Assert.assertEquals(2, batches.get(0).size()); + Assert.assertEquals("alice", batches.get(0).get(0).getValue()); + Assert.assertEquals("bob", batches.get(0).get(1).getValue()); + Assert.assertEquals("alice", batches.get(1).get(0).getValue()); + Assert.assertEquals("carol", batches.get(1).get(1).getValue()); + } + + @Test + public void testBatching_stillHonoursBatchSize() { + List records = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + records.add(record("v" + i, "g1")); + } + + List> batches = Utils.createBulkTokenizeBatches(records, 2); + + Assert.assertEquals(3, batches.size()); + Assert.assertEquals(2, batches.get(0).size()); + Assert.assertEquals(2, batches.get(1).size()); + Assert.assertEquals(1, batches.get(2).size()); + } + + @Test + public void testBatching_batchesStayContiguousSoIndexesFollowFromTheStart() { + List records = Arrays.asList( + record("dup", "g1"), record("dup", "g1"), record("dup", "g1")); + + List> batches = Utils.createBulkTokenizeBatches(records, 10); + + // three copies of one value cannot share a request, so each gets its own + Assert.assertEquals(3, batches.size()); + int seen = 0; + for (List batch : batches) { + seen += batch.size(); + } + Assert.assertEquals(records.size(), seen); + } + + @Test + public void testBatching_emptyAndNullInputs() { + Assert.assertTrue(Utils.createBulkTokenizeBatches(null, 10).isEmpty()); + Assert.assertTrue(Utils.createBulkTokenizeBatches(Collections.emptyList(), 10).isEmpty()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java b/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java index de1f3b40..13e2506e 100644 --- a/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java @@ -22,7 +22,7 @@ import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertResponse; -import com.skyflow.vault.data.BulkTokenizeRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.TokenGroupRedactions; @@ -259,9 +259,9 @@ public void testBulkInsert_responseIndexMapsToOriginalInputPosition_acrossBatche // ── insert: table-name precedence / fallback regressions ───────────────── @Test - public void testBulkInsert_blankRecordTableNameFallsBackToRequestLevel() { - // Regression: a blank record-level table name counts as ABSENT (Utils.hasText), so the - // request-level name must be sent — not the literal whitespace string. + public void testBulkInsert_blankRecordTableNameStaysAtRequestLevel() { + // Regression: a blank record-level table name counts as ABSENT (Utils.hasText). The name + // must go out on the envelope ONLY — the vault rejects a body carrying it at both levels. Map data = new HashMap<>(); data.put("name", "john"); BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() @@ -275,12 +275,12 @@ public void testBulkInsert_blankRecordTableNameFallsBackToRequestLevel() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); - Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); Assert.assertEquals("cards", body.getTableName().get()); } @Test - public void testBulkInsert_nullRecordTableNameFallsBackToRequestLevel() { + public void testBulkInsert_nullRecordTableNameStaysAtRequestLevel() { Map data = new HashMap<>(); data.put("name", "john"); BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); @@ -291,11 +291,12 @@ public void testBulkInsert_nullRecordTableNameFallsBackToRequestLevel() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); - Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals("cards", body.getTableName().get()); } @Test - public void testBulkInsert_emptyStringRecordTableNameFallsBackToRequestLevel() { + public void testBulkInsert_emptyStringRecordTableNameStaysAtRequestLevel() { Map data = new HashMap<>(); data.put("name", "john"); BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("").data(data).build(); @@ -306,7 +307,8 @@ public void testBulkInsert_emptyStringRecordTableNameFallsBackToRequestLevel() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); - Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals("cards", body.getTableName().get()); } @Test @@ -343,9 +345,12 @@ public void testBulkInsert_perRecordTableNamesResolveIndependently() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + // Only a record that names its own table carries one on the wire; the others rely on the + // envelope. Nothing is copied down, so the name is never duplicated across both levels. Assert.assertEquals("own", body.getRecords().get().get(0).getTableName().get()); - Assert.assertEquals("fallback", body.getRecords().get().get(1).getTableName().get()); - Assert.assertEquals("fallback", body.getRecords().get().get(2).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(1).getTableName().isPresent()); + Assert.assertFalse(body.getRecords().get().get(2).getTableName().isPresent()); + Assert.assertEquals("fallback", body.getTableName().get()); } @Test @@ -437,7 +442,7 @@ public void testUpsert_recordLevelOverridesRequestLevel() { } @Test - public void testUpsert_requestLevelAppliesToEveryRecord() { + public void testUpsert_requestLevelStaysOnEnvelopeAndIsNotCopiedOntoRecords() { Map data = new HashMap<>(); data.put("name", "john"); BulkInsertRequest request = BulkInsertRequest.builder() @@ -454,20 +459,20 @@ public void testUpsert_requestLevelAppliesToEveryRecord() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + // upsert must travel at the same single level as the table name — here, the envelope. for (V1InsertRecordData wire : body.getRecords().get()) { - Assert.assertTrue(wire.getUpsert().isPresent()); - Assert.assertEquals(FlowEnumUpdateType.UPDATE, wire.getUpsert().get().getUpdateType().get()); - Assert.assertEquals(Collections.singletonList("email"), wire.getUpsert().get().getUniqueColumns().get()); + Assert.assertFalse(wire.getUpsert().isPresent()); } + Assert.assertTrue(body.getUpsert().isPresent()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getUpsert().get().getUpdateType().get()); + Assert.assertEquals(Collections.singletonList("email"), body.getUpsert().get().getUniqueColumns().get()); } @Test - public void testUpsert_requestLevelUpsertNeverReachesEnvelope_knownGap() { - // KNOWN GAP: Utils.getInsertRequestBody projects the request-level upsert onto each - // record but never sets it on the V1InsertRequest envelope. VaultController#insertBatchFutures - // reads insertRequest.getUpsert() to re-apply it to every batch request, so that read is - // always empty. Harmless today (each record already carries it), but the envelope-level - // upsert is dead wiring. + public void testUpsert_requestLevelUpsertReachesEnvelope() { + // Regression: the request-level upsert used to be projected onto every record and never set + // on the V1InsertRequest envelope, so VaultController#insertBatchFutures — which reads + // insertRequest.getUpsert() to re-apply it per batch — always read empty. Map data = new HashMap<>(); data.put("name", "john"); BulkInsertRequest request = BulkInsertRequest.builder() @@ -481,8 +486,8 @@ public void testUpsert_requestLevelUpsertNeverReachesEnvelope_knownGap() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); - Assert.assertFalse(body.getUpsert().isPresent()); - Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertTrue(body.getUpsert().isPresent()); + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); } @Test @@ -718,14 +723,13 @@ public void testBulkDetokenize_responseIndexMapsToOriginalInputPosition_acrossBa @Test public void testBulkTokenize_everyFieldReachesWire() { List groupNames = Arrays.asList("group one", NON_ASCII_NAME); - BulkTokenizeRecord record = BulkTokenizeRecord.builder() + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder() .value(NON_ASCII_NAME) .tokenGroupNames(groupNames) .build(); - ArrayList data = new ArrayList<>(Collections.singletonList(record)); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + List records = Collections.singletonList(record); - V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, VAULT_ID); Assert.assertEquals(VAULT_ID, body.getVaultId().get()); Assert.assertEquals(1, body.getData().get().size()); @@ -735,6 +739,34 @@ public void testBulkTokenize_everyFieldReachesWire() { Assert.assertEquals(Arrays.asList("group one", NON_ASCII_NAME), wire.getTokenGroupNames().get()); } + @Test + public void testBulkTokenize_byotTokenReachesWire() { + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder() + .value("v1") + .token("my-own-token") + .tokenGroupNames(Collections.singletonList("g1")) + .build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody( + Collections.singletonList(record), VAULT_ID); + + Assert.assertEquals("my-own-token", body.getData().get().get(0).getToken().get()); + } + + @Test + public void testBulkTokenize_absentByotTokenIsOmittedFromWire() { + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder() + .value("v1") + .tokenGroupNames(Collections.singletonList("g1")) + .build(); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody( + Collections.singletonList(record), VAULT_ID); + + // omitted rather than sent as null, so a non-BYOT request is byte-identical to before + Assert.assertFalse(body.getData().get().get(0).getToken().isPresent()); + } + @Test public void testBulkTokenize_nonStringValues_objectIdentitySurvives() { Map nested = new LinkedHashMap<>(); @@ -742,15 +774,14 @@ public void testBulkTokenize_nonStringValues_objectIdentitySurvives() { nested.put("zip", 75001); List listValue = Arrays.asList(1, 2, 3); - ArrayList data = new ArrayList<>(Arrays.asList( - BulkTokenizeRecord.builder().value(42).build(), - BulkTokenizeRecord.builder().value(3.14d).build(), - BulkTokenizeRecord.builder().value(Boolean.FALSE).build(), - BulkTokenizeRecord.builder().value(nested).build(), - BulkTokenizeRecord.builder().value(listValue).build())); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + List records = Arrays.asList( + BulkTokenizeRequestRecord.builder().value(42).build(), + BulkTokenizeRequestRecord.builder().value(3.14d).build(), + BulkTokenizeRequestRecord.builder().value(Boolean.FALSE).build(), + BulkTokenizeRequestRecord.builder().value(nested).build(), + BulkTokenizeRequestRecord.builder().value(listValue).build()); - V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, VAULT_ID); List wire = body.getData().get(); Assert.assertEquals(Integer.valueOf(42), wire.get(0).getValue().get()); @@ -762,11 +793,10 @@ public void testBulkTokenize_nonStringValues_objectIdentitySurvives() { @Test public void testBulkTokenize_nullTokenGroupNamesIsOmitted() { - ArrayList data = new ArrayList<>( - Collections.singletonList(BulkTokenizeRecord.builder().value("v1").build())); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + List records = + Collections.singletonList(BulkTokenizeRequestRecord.builder().value("v1").build()); - V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, VAULT_ID); Assert.assertFalse(body.getData().get().get(0).getTokenGroupNames().isPresent()); } @@ -775,21 +805,23 @@ public void testBulkTokenize_nullTokenGroupNamesIsOmitted() { public void testBulkTokenize_recordOrderPreservedAcrossBatches() { int total = 7; int batchSize = 3; - ArrayList data = new ArrayList<>(); + List records = new ArrayList<>(); for (int i = 0; i < total; i++) { - data.add(BulkTokenizeRecord.builder().value("value-" + i).build()); + records.add(BulkTokenizeRequestRecord.builder().value("value-" + i).build()); } - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); - V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, VAULT_ID); - List batches = Utils.createBulkTokenizeBatches(body, batchSize); + // batching now happens on the SDK records, before the wire object is built, so the + // response formatter can recover each value's index from its batch position + List> batches = + Utils.createBulkTokenizeBatches(records, batchSize); Assert.assertEquals(3, batches.size()); List flattened = new ArrayList<>(); - for (V1FlowTokenizeRequest batch : batches) { + for (List batch : batches) { + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(batch, VAULT_ID); // vaultId is a non-batched field and must be re-applied on every batch. - Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); - for (V1FlowTokenizeRequestObject obj : batch.getData().get()) { + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + for (V1FlowTokenizeRequestObject obj : body.getData().get()) { flattened.add(obj.getValue().get()); } } @@ -858,11 +890,12 @@ public void testBulkDeleteTokens_responseIndexMapsToOriginalInputPosition_across } V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder().tokens(responseRecords).build(); - BulkDeleteTokensResponse formatted = - Utils.formatBulkDeleteTokensResponse(response, batchNumber, batchSize, new HashMap<>()); - formatted.getSuccess().forEach(s -> { - indices.add(s.getIndex()); - echoed.add(s.getToken()); + // successes and errors now share one records list, keyed by index + BulkDeleteTokensResponse formatted = Utils.formatBulkDeleteTokensResponse( + response, batches.get(batchNumber), batchNumber, batchSize, new HashMap<>()); + formatted.getRecords().forEach(r -> { + indices.add(r.getIndex()); + echoed.add(r.getToken()); }); } diff --git a/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java b/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java new file mode 100644 index 00000000..e5e56857 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java @@ -0,0 +1,415 @@ +package com.skyflow.utils; + +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.utils.BaseConstants; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.TokenizeResponseToken; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The API does not report a request id per record; it comes back once per call in the + * {@code x-request-id} response header. Bulk operations split the caller's list across several + * calls, so the SDK stamps each error with the id of the call it came from - and only errors, since + * that is when it is useful for support. + * + *

These cover the two properties that matter: every error from one batch shares a single id, and + * errors from different batches carry different ones. + */ +public class RequestIdTests { + + private static final String REQ_ID_A = "req-aaaa-1111"; + private static final String REQ_ID_B = "req-bbbb-2222"; + + private static Map> headers(String requestId) { + Map> headers = new HashMap<>(); + if (requestId != null) { + headers.put(BaseConstants.REQUEST_ID_HEADER_KEY, Collections.singletonList(requestId)); + } + return headers; + } + + /** + * Builds the exception the SDK actually sees on a non-2xx, carrying a real okhttp response so + * the header is read the same way it is in production rather than injected. + */ + private static ApiClientApiException apiException(String message, int status, Object body, + String requestId) { + Response.Builder raw = new Response.Builder() + .request(new Request.Builder().url("https://vault.example.test/v1/tokenize").build()) + .protocol(Protocol.HTTP_1_1) + .code(status) + .message(message); + if (requestId != null) { + raw.header(BaseConstants.REQUEST_ID_HEADER_KEY, requestId); + } + return new ApiClientApiException(message, status, body, raw.build()); + } + + private static V1FlowTokenizeResponse tokenizeWire(String json) { + try { + return ObjectMappers.JSON_MAPPER.readValue(json, V1FlowTokenizeResponse.class); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static BulkTokenizeRequestRecord tokenizeRecord(Object value, String... groups) { + return BulkTokenizeRequestRecord.builder() + .value(value).tokenGroupNames(Arrays.asList(groups)).build(); + } + + // ── tokenize: success carries no id, errors carry the batch's ────────────── + + @Test + public void testTokenize_requestIdOnErrorsOnly() { + String json = "{\"response\": [" + + "{\"value\":\"ok\",\"tokenGroupName\":\"g1\",\"token\":\"tok-1\",\"httpCode\":200,\"error\":null}," + + "{\"value\":\"bad\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400," + + " \"error\":\"Token group g9 is invalid.\"}" + + "]}"; + List sent = Arrays.asList( + tokenizeRecord("ok", "g1"), tokenizeRecord("bad", "g9")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + tokenizeWire(json), sent, 0, headers(REQ_ID_A)); + + TokenizeResponseToken success = result.getRecords().get(0).getTokens().get(0); + TokenizeResponseToken failure = result.getRecords().get(1).getTokens().get(0); + Assert.assertNull("a successful token must not carry a request id", success.getRequestId()); + Assert.assertEquals(REQ_ID_A, failure.getRequestId()); + } + + @Test + public void testTokenize_everyErrorInOneBatchSharesOneRequestId() { + String json = "{\"response\": [" + + "{\"value\":\"v0\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}," + + "{\"value\":\"v1\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}," + + "{\"value\":\"v2\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}" + + "]}"; + List sent = Arrays.asList( + tokenizeRecord("v0", "g1"), tokenizeRecord("v1", "g1"), tokenizeRecord("v2", "g1")); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + tokenizeWire(json), sent, 0, headers(REQ_ID_A)); + + Set ids = tokenizeRequestIds(result.getRecords()); + Assert.assertEquals(Collections.singleton(REQ_ID_A), ids); + } + + @Test + public void testTokenize_twoBatchesReportTwoDistinctRequestIds() { + String batchJson = "{\"response\": [" + + "{\"value\":\"%s\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}," + + "{\"value\":\"%s\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}" + + "]}"; + + // batch 0 occupies indexes 0-1 and answers with REQ_ID_A + BulkTokenizeResponse first = Utils.formatBulkTokenizeResponse( + tokenizeWire(String.format(batchJson, "v0", "v1")), + Arrays.asList(tokenizeRecord("v0", "g1"), tokenizeRecord("v1", "g1")), + 0, headers(REQ_ID_A)); + // batch 1 occupies indexes 2-3 and answers with REQ_ID_B + BulkTokenizeResponse second = Utils.formatBulkTokenizeResponse( + tokenizeWire(String.format(batchJson, "v2", "v3")), + Arrays.asList(tokenizeRecord("v2", "g1"), tokenizeRecord("v3", "g1")), + 2, headers(REQ_ID_B)); + + Assert.assertEquals(Collections.singleton(REQ_ID_A), tokenizeRequestIds(first.getRecords())); + Assert.assertEquals(Collections.singleton(REQ_ID_B), tokenizeRequestIds(second.getRecords())); + + // merged, as the controller returns them: index tells you the record, requestId the call + List merged = new ArrayList<>(first.getRecords()); + merged.addAll(second.getRecords()); + Assert.assertEquals(4, merged.size()); + Assert.assertEquals(REQ_ID_A, merged.get(0).getTokens().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, merged.get(1).getTokens().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(2).getTokens().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(3).getTokens().get(0).getRequestId()); + Assert.assertEquals(2, merged.get(2).getIndex()); + Assert.assertEquals(3, merged.get(3).getIndex()); + } + + @Test + public void testTokenize_missingHeaderLeavesRequestIdNull() { + String json = "{\"response\": [{\"value\":\"v0\",\"tokenGroupName\":null,\"token\":\"\"," + + "\"httpCode\":400,\"error\":\"bad group\"}]}"; + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + tokenizeWire(json), Collections.singletonList(tokenizeRecord("v0", "g1")), + 0, headers(null)); + + Assert.assertNull(result.getRecords().get(0).getTokens().get(0).getRequestId()); + } + + @Test + public void testTokenize_partialRecordStampsOnlyTheFailedGroup() { + String json = "{\"response\": [" + + "{\"value\":\"v0\",\"tokenGroupName\":\"g1\",\"token\":\"tok-1\",\"httpCode\":200,\"error\":null}," + + "{\"value\":\"v0\",\"tokenGroupName\":null,\"token\":\"\",\"httpCode\":400,\"error\":\"bad group\"}" + + "]}"; + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + tokenizeWire(json), Collections.singletonList(tokenizeRecord("v0", "g1", "g2")), + 0, headers(REQ_ID_A)); + + List tokens = result.getRecords().get(0).getTokens(); + Assert.assertEquals(2, tokens.size()); + Assert.assertNull(tokens.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, tokens.get(1).getRequestId()); + } + + // ── tokenize: rejected requests ──────────────────────────────────────────── + + @Test + public void testTokenize_rejectedRequestStampsIdFromTheFailedCall() { + Map row = new LinkedHashMap<>(); + row.put("token", ""); + row.put("value", "v0"); + row.put("tokenGroupName", null); + row.put("error", "Invalid request. BYOT token should contain one token group."); + row.put("httpCode", 400); + Map body = new LinkedHashMap<>(); + body.put("response", Collections.singletonList(row)); + + Throwable ex = new RuntimeException( + apiException("Error with status code 400", 400, body, REQ_ID_B)); + + List records = Utils.handleBulkTokenizeBatchException( + ex, Collections.singletonList(tokenizeRecord("v0", "g1")), 0); + + Assert.assertEquals(REQ_ID_B, records.get(0).getTokens().get(0).getRequestId()); + } + + @Test + public void testTokenize_rejectedRequestWithNoBodyStillStampsEveryGroup() { + Throwable ex = new RuntimeException( + apiException("boom", 503, null, REQ_ID_A)); + + List records = Utils.handleBulkTokenizeBatchException( + ex, Collections.singletonList(tokenizeRecord("v0", "g1", "g2")), 0); + + List tokens = records.get(0).getTokens(); + Assert.assertEquals(2, tokens.size()); + Assert.assertEquals(REQ_ID_A, tokens.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, tokens.get(1).getRequestId()); + } + + @Test + public void testTokenize_transportFailureHasNoRequestId() { + // never reached the API, so there is no call to point at + List records = Utils.handleBulkTokenizeBatchException( + new RuntimeException("connection reset"), + Collections.singletonList(tokenizeRecord("v0", "g1")), 0); + + Assert.assertNull(records.get(0).getTokens().get(0).getRequestId()); + Assert.assertEquals("connection reset", records.get(0).getTokens().get(0).getError()); + } + + // ── delete: success carries no id, errors carry the batch's ──────────────── + + private static V1FlowDeleteTokenResponse deleteWire(V1DeleteTokenResponseObject... rows) { + return V1FlowDeleteTokenResponse.builder().tokens(Arrays.asList(rows)).build(); + } + + private static V1DeleteTokenResponseObject deleted(String token) { + return V1DeleteTokenResponseObject.builder().value(token).httpCode(200).build(); + } + + private static V1DeleteTokenResponseObject failed(String token, String error) { + return V1DeleteTokenResponseObject.builder().value(token).error(error).httpCode(404).build(); + } + + private static V1FlowDeleteTokenRequest deleteBatch(String... tokens) { + return V1FlowDeleteTokenRequest.builder() + .vaultId("vault123").tokens(Arrays.asList(tokens)).build(); + } + + @Test + public void testDelete_requestIdOnErrorsOnly() { + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + deleteWire(deleted("t0"), failed("t1", "Token t1 is invalid.")), + deleteBatch("t0", "t1"), 0, 50, headers(REQ_ID_A)); + + Assert.assertNull("a deleted token must not carry a request id", + result.getRecords().get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, result.getRecords().get(1).getRequestId()); + } + + @Test + public void testDelete_everyErrorInOneBatchSharesOneRequestId() { + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + deleteWire(failed("t0", "invalid"), failed("t1", "invalid"), failed("t2", "invalid")), + deleteBatch("t0", "t1", "t2"), 0, 50, headers(REQ_ID_A)); + + Assert.assertEquals(Collections.singleton(REQ_ID_A), deleteRequestIds(result.getRecords())); + } + + @Test + public void testDelete_twoBatchesReportTwoDistinctRequestIds() { + // batch 0 of size 2 covers indexes 0-1 + BulkDeleteTokensResponse first = Utils.formatBulkDeleteTokensResponse( + deleteWire(failed("t0", "invalid"), failed("t1", "invalid")), + deleteBatch("t0", "t1"), 0, 2, headers(REQ_ID_A)); + // batch 1 of size 2 covers indexes 2-3 + BulkDeleteTokensResponse second = Utils.formatBulkDeleteTokensResponse( + deleteWire(failed("t2", "invalid"), failed("t3", "invalid")), + deleteBatch("t2", "t3"), 1, 2, headers(REQ_ID_B)); + + Assert.assertEquals(Collections.singleton(REQ_ID_A), deleteRequestIds(first.getRecords())); + Assert.assertEquals(Collections.singleton(REQ_ID_B), deleteRequestIds(second.getRecords())); + + List merged = new ArrayList<>(first.getRecords()); + merged.addAll(second.getRecords()); + Assert.assertEquals(4, merged.size()); + Assert.assertEquals(REQ_ID_A, merged.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, merged.get(1).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(2).getRequestId()); + Assert.assertEquals(REQ_ID_B, merged.get(3).getRequestId()); + Assert.assertEquals(2, merged.get(2).getIndex()); + Assert.assertEquals(3, merged.get(3).getIndex()); + } + + @Test + public void testDelete_mixedBatchStampsOnlyTheFailures() { + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + deleteWire(deleted("t0"), failed("t1", "invalid"), deleted("t2"), failed("t3", "invalid")), + deleteBatch("t0", "t1", "t2", "t3"), 0, 50, headers(REQ_ID_A)); + + List records = result.getRecords(); + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals(REQ_ID_A, records.get(1).getRequestId()); + Assert.assertNull(records.get(2).getRequestId()); + Assert.assertEquals(REQ_ID_A, records.get(3).getRequestId()); + } + + @Test + public void testDelete_missingHeaderLeavesRequestIdNull() { + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + deleteWire(failed("t0", "invalid")), deleteBatch("t0"), 0, 50, headers(null)); + + Assert.assertNull(result.getRecords().get(0).getRequestId()); + } + + // ── delete: rejected requests ────────────────────────────────────────────── + + @Test + public void testDelete_rejectedRequestWithPerTokenBodyStampsEveryRecord() { + Map row = new LinkedHashMap<>(); + row.put("value", "t0"); + row.put("error", "Token t0 is invalid."); + row.put("httpCode", 404); + Map body = new LinkedHashMap<>(); + body.put("tokens", Collections.singletonList(row)); + + Throwable ex = new RuntimeException( + apiException("Error with status code 404", 404, body, REQ_ID_B)); + + List records = + Utils.handleBulkDeleteTokensBatchException(ex, deleteBatch("t0"), 0, 50); + + Assert.assertEquals(REQ_ID_B, records.get(0).getRequestId()); + } + + @Test + public void testDelete_rejectedRequestWithNoBodyStampsEveryToken() { + Throwable ex = new RuntimeException( + apiException("boom", 503, null, REQ_ID_A)); + + List records = + Utils.handleBulkDeleteTokensBatchException(ex, deleteBatch("t0", "t1"), 0, 50); + + Assert.assertEquals(2, records.size()); + Assert.assertEquals(Collections.singleton(REQ_ID_A), deleteRequestIds(records)); + } + + @Test + public void testDelete_rejectedBatchesKeepTheirOwnRequestIds() { + Throwable exA = new RuntimeException( + apiException("boom", 503, null, REQ_ID_A)); + Throwable exB = new RuntimeException( + apiException("boom", 503, null, REQ_ID_B)); + + List first = + Utils.handleBulkDeleteTokensBatchException(exA, deleteBatch("t0", "t1"), 0, 2); + List second = + Utils.handleBulkDeleteTokensBatchException(exB, deleteBatch("t2", "t3"), 1, 2); + + Assert.assertEquals(Collections.singleton(REQ_ID_A), deleteRequestIds(first)); + Assert.assertEquals(Collections.singleton(REQ_ID_B), deleteRequestIds(second)); + Assert.assertEquals(2, second.get(0).getIndex()); + Assert.assertEquals(3, second.get(1).getIndex()); + } + + @Test + public void testDelete_transportFailureHasNoRequestId() { + List records = Utils.handleBulkDeleteTokensBatchException( + new RuntimeException("connection reset"), deleteBatch("t0"), 0, 50); + + Assert.assertNull(records.get(0).getRequestId()); + Assert.assertEquals("connection reset", records.get(0).getError()); + } + + // ── JSON output ─────────────────────────────────────────────────────────── + + @Test + public void testRequestIdAppearsInToString() { + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + deleteWire(deleted("t0"), failed("t1", "invalid")), + deleteBatch("t0", "t1"), 0, 50, headers(REQ_ID_A)); + + String json = result.toString(); + Assert.assertTrue("errors must expose the id to callers reading the JSON", + json.contains("\"requestId\":\"" + REQ_ID_A + "\"")); + Assert.assertTrue("successes must show it as null rather than omit it", + json.contains("\"requestId\":null")); + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + /** Every non-null request id across a tokenize result's error entries. */ + private static Set tokenizeRequestIds(List records) { + Set ids = new HashSet<>(); + for (BulkTokenizeResponseRecord record : records) { + for (TokenizeResponseToken token : record.getTokens()) { + if (token.getError() != null) { + Assert.assertNotNull("every error must carry a request id", token.getRequestId()); + ids.add(token.getRequestId()); + } + } + } + return ids; + } + + /** Every non-null request id across a delete result's error records. */ + private static Set deleteRequestIds(List records) { + Set ids = new HashSet<>(); + for (BulkDeleteTokensResponseRecord record : records) { + if (record.getError() != null) { + Assert.assertNotNull("every error must carry a request id", record.getRequestId()); + ids.add(record.getRequestId()); + } + } + return ids; + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/SkyflowRetryInterceptorTests.java b/flowvault/src/test/java/com/skyflow/utils/SkyflowRetryInterceptorTests.java new file mode 100644 index 00000000..aae9a6f9 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/SkyflowRetryInterceptorTests.java @@ -0,0 +1,254 @@ +package com.skyflow.utils; + +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; + +/** + * Internals of the retry interceptor. Lives in com.skyflow.utils so the package-private + * backoff/should-retry helpers stay off the public API surface. + */ +public class SkyflowRetryInterceptorTests { + + @Test + public void testConstructor_rejectsNegativeMaxRetries() { + try { + new SkyflowRetryInterceptor(-1, 500L, 2000L); + Assert.fail("negative maxRetries should be rejected"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("maxRetries")); + } + } + + @Test + public void testConstructor_rejectsNegativeInitialDelay() { + try { + new SkyflowRetryInterceptor(1, -1L, 2000L); + Assert.fail("negative initialRetryDelayMillis should be rejected"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("initialRetryDelayMillis")); + } + } + + @Test + public void testConstructor_rejectsNegativeMaxDelay() { + try { + new SkyflowRetryInterceptor(1, 500L, -1L); + Assert.fail("negative maxRetryDelayMillis should be rejected"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("maxRetryDelayMillis")); + } + } + + @Test + public void testBackoff_growsExponentiallyThenCaps() { + // Jitter is +/-20%, so assert bands rather than exact values. + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(5, 100L, 400L); + + assertWithinJitter(100L, retry.backoffMillis(1)); + assertWithinJitter(200L, retry.backoffMillis(2)); + assertWithinJitter(400L, retry.backoffMillis(3)); + assertWithinJitter(400L, retry.backoffMillis(4)); + assertWithinJitter(400L, retry.backoffMillis(10)); + } + + @Test + public void testBackoff_neverExceedsTheCapAcrossManyDraws() { + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(5, 100L, 400L); + + for (int i = 0; i < 200; i++) { + long delay = retry.backoffMillis(3); + Assert.assertTrue("jittered delay went negative: " + delay, delay >= 0); + Assert.assertTrue("jittered delay exceeded cap + jitter: " + delay, delay <= 480L); + } + } + + @Test + public void testBackoff_zeroDelayStaysZero() { + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(3, 0L, 0L); + + Assert.assertEquals(0L, retry.backoffMillis(1)); + Assert.assertEquals(0L, retry.backoffMillis(5)); + } + + @Test + public void testBackoff_initialDelayAboveCapIsClampedToCap() { + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(3, 5000L, 1000L); + + assertWithinJitter(1000L, retry.backoffMillis(1)); + assertWithinJitter(1000L, retry.backoffMillis(3)); + } + + @Test + public void testShouldRetry_retryableStatuses() { + Assert.assertTrue(SkyflowRetryInterceptor.shouldRetry(408)); + Assert.assertTrue(SkyflowRetryInterceptor.shouldRetry(429)); + Assert.assertTrue(SkyflowRetryInterceptor.shouldRetry(500)); + Assert.assertTrue(SkyflowRetryInterceptor.shouldRetry(502)); + Assert.assertTrue(SkyflowRetryInterceptor.shouldRetry(503)); + } + + @Test + public void testShouldRetry_nonRetryableStatuses() { + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(200)); + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(201)); + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(400)); + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(401)); + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(404)); + Assert.assertFalse(SkyflowRetryInterceptor.shouldRetry(409)); + } + + @Test + public void testAccessors_reportWhatWasConfigured() { + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(3, 100L, 900L); + + Assert.assertEquals(3, retry.getMaxRetries()); + Assert.assertEquals(100L, retry.getInitialRetryDelayMillis()); + Assert.assertEquals(900L, retry.getMaxRetryDelayMillis()); + } + + // ── intercept(): the retry loop ─────────────────────────────────────────── + // Delays are set to 0 so these do not actually sleep. + + private static SkyflowRetryInterceptor retrying(int maxRetries) { + return new SkyflowRetryInterceptor(maxRetries, 0L, 0L); + } + + @Test + public void testIntercept_successFirstTimeIsNotRetried() throws IOException { + FakeChain chain = new FakeChain(200); + + Response response = retrying(3).intercept(chain); + + Assert.assertEquals(1, chain.calls()); + Assert.assertEquals(200, response.code()); + } + + @Test + public void testIntercept_nonRetryableFailureIsNotRetried() throws IOException { + FakeChain chain = new FakeChain(400); + + Response response = retrying(3).intercept(chain); + + Assert.assertEquals("a 400 must not be replayed", 1, chain.calls()); + Assert.assertEquals(400, response.code()); + } + + @Test + public void testIntercept_retriesUpToTheBudgetThenReturnsTheLastFailure() throws IOException { + FakeChain chain = new FakeChain(500); + + Response response = retrying(2).intercept(chain); + + Assert.assertEquals("1 initial attempt + 2 retries", 3, chain.calls()); + Assert.assertEquals(500, response.code()); + } + + @Test + public void testIntercept_stopsAsSoonAsAnAttemptSucceeds() throws IOException { + FakeChain chain = new FakeChain(503, 200, 200); + + Response response = retrying(5).intercept(chain); + + Assert.assertEquals("must not keep retrying after success", 2, chain.calls()); + Assert.assertEquals(200, response.code()); + } + + @Test + public void testIntercept_stopsOnANonRetryableStatusMidWay() throws IOException { + FakeChain chain = new FakeChain(500, 404, 200); + + Response response = retrying(5).intercept(chain); + + Assert.assertEquals(2, chain.calls()); + Assert.assertEquals(404, response.code()); + } + + @Test + public void testIntercept_zeroBudgetMeansNoRetryAtAll() throws IOException { + FakeChain chain = new FakeChain(500); + + Response response = retrying(0).intercept(chain); + + Assert.assertEquals(1, chain.calls()); + Assert.assertEquals(500, response.code()); + } + + @Test + public void testIntercept_retriesEachRetryableStatus() throws IOException { + for (int code : new int[] {408, 429, 500, 502, 503}) { + FakeChain chain = new FakeChain(code, 200); + + Response response = retrying(1).intercept(chain); + + Assert.assertEquals("should have retried a " + code, 2, chain.calls()); + Assert.assertEquals(200, response.code()); + } + } + + @Test + public void testIntercept_closesEverySupersededResponse() throws IOException { + // Leaking the body of a response we are about to discard would leak the connection. + FakeChain chain = new FakeChain(500, 500, 200); + + retrying(2).intercept(chain); + + Assert.assertEquals(3, chain.bodies().size()); + Assert.assertTrue("first failed response not closed", chain.bodies().get(0).closed); + Assert.assertTrue("second failed response not closed", chain.bodies().get(1).closed); + Assert.assertFalse("the returned response must stay open", chain.bodies().get(2).closed); + } + + @Test + public void testIntercept_retryBudgetIsPerCallNotPerInterceptorInstance() throws IOException { + // The generated RetryInterceptor keeps its backoff counter on the instance, so one shared + // instance exhausts the budget once for the whole client. A single interceptor is installed + // on a shared OkHttpClient, so every call must get its own full budget. + SkyflowRetryInterceptor retry = retrying(2); + + FakeChain first = new FakeChain(500); + retry.intercept(first); + FakeChain second = new FakeChain(500); + retry.intercept(second); + FakeChain third = new FakeChain(500); + retry.intercept(third); + + Assert.assertEquals(3, first.calls()); + Assert.assertEquals("second call lost its retry budget", 3, second.calls()); + Assert.assertEquals("third call lost its retry budget", 3, third.calls()); + } + + @Test + public void testIntercept_interruptionSurfacesAsIOException() { + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(2, 5_000L, 5_000L); + FakeChain chain = new FakeChain(500); + + Thread.currentThread().interrupt(); + try { + retry.intercept(chain); + Assert.fail("an interrupt while backing off should surface as IOException"); + } catch (IOException e) { + Assert.assertTrue(e.getMessage().contains("Interrupted")); + Assert.assertTrue("the interrupt flag must be restored", Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); // clear the flag so it cannot leak into another test + } + } + + @Test + public void testBackoff_growsPastHalfTheCapInOneStep() { + // initial > max/2, so the next step clamps straight to the cap instead of doubling past it. + SkyflowRetryInterceptor retry = new SkyflowRetryInterceptor(3, 300L, 400L); + + assertWithinJitter(300L, retry.backoffMillis(1)); + assertWithinJitter(400L, retry.backoffMillis(2)); + } + + private static void assertWithinJitter(long expected, long actual) { + long jitter = (long) (expected * 0.2); + Assert.assertTrue("expected ~" + expected + " (+/-" + jitter + ") but got " + actual, + actual >= expected - jitter && actual <= expected + jitter); + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java index d2624bbc..d220084a 100644 --- a/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -23,22 +23,29 @@ import com.skyflow.generated.rest.types.V1RecordResponseObject; import com.skyflow.vault.data.BulkDeleteTokensRequest; import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; import com.skyflow.vault.data.BulkDetokenizeRequest; import com.skyflow.vault.data.BulkDetokenizeResponse; import com.skyflow.vault.data.BulkDetokenizeResponseRecord; import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkInsertResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; import com.skyflow.vault.data.ErrorRecord; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.TokenizeResponseRecord; +import com.skyflow.vault.data.TokenizeRequestRecord; +import com.skyflow.vault.data.TokenizeRequest; +import com.skyflow.vault.data.TokenizeResponse; import com.skyflow.vault.data.UpsertOptions; import org.junit.After; +import com.skyflow.vault.data.TokenizeResponseToken; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -235,7 +242,7 @@ public void testGetInsertRequestBody_buildsCorrectRequest() { } @Test - public void testGetInsertRequestBody_fallsBackToRequestLevelTableName() { + public void testGetInsertRequestBody_keepsRequestLevelTableNameOnEnvelopeOnly() { Map data = new HashMap<>(); data.put("name", "john"); InsertRequestRecord record = InsertRequestRecord.builder().data(data).build(); @@ -248,7 +255,7 @@ public void testGetInsertRequestBody_fallsBackToRequestLevelTableName() { V1InsertRequest body = Utils.getInsertRequestBody(request, config); Assert.assertEquals("table1", body.getTableName().get()); - Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); } @Test @@ -289,9 +296,11 @@ public void testGetInsertRequestBody_withUpsertAtRequestLevel() { V1InsertRequest body = Utils.getInsertRequestBody(request, config); - Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); - Assert.assertEquals(Collections.singletonList("email"), body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get()); - Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); + // Request-level upsert stays on the envelope; it is not copied onto the records. + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertTrue(body.getUpsert().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), body.getUpsert().get().getUniqueColumns().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getUpsert().get().getUpdateType().get()); } @Test @@ -342,7 +351,7 @@ public void testGetBulkInsertRequestBody_bulk_buildsCorrectRequest() { Assert.assertEquals("vault123", body.getVaultId().get()); Assert.assertEquals("table1", body.getTableName().get()); Assert.assertEquals(1, body.getRecords().get().size()); - Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); } @@ -366,9 +375,11 @@ public void testGetBulkInsertRequestBody_bulk_withUpsertAtRequestLevel() { V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); - Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); - Assert.assertEquals(Collections.singletonList("email"), body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get()); - Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); + // Request-level upsert stays on the envelope; it is not copied onto the records. + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertTrue(body.getUpsert().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), body.getUpsert().get().getUniqueColumns().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, body.getUpsert().get().getUpdateType().get()); } @Test @@ -448,20 +459,33 @@ public void testGetBulkDeleteTokensRequestBody_buildsCorrectRequest() { @Test public void testGetBulkTokenizeRequestBody_buildsCorrectRequest() { - BulkTokenizeRecord record = BulkTokenizeRecord.builder() - .value("value1") - .tokenGroupNames(Collections.singletonList("group1")) - .build(); - ArrayList data = new ArrayList<>(); - data.add(record); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder() + .value("value1") + .tokenGroupNames(Collections.singletonList("group1")) + .build()); - V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(request, "vault123"); + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, "vault123"); Assert.assertEquals("vault123", body.getVaultId().get()); Assert.assertEquals(1, body.getData().get().size()); Assert.assertEquals("value1", body.getData().get().get(0).getValue().get()); Assert.assertEquals(Collections.singletonList("group1"), body.getData().get().get(0).getTokenGroupNames().get()); + Assert.assertFalse(body.getData().get().get(0).getToken().isPresent()); + } + + @Test + public void testGetBulkTokenizeRequestBody_carriesByotToken() { + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder() + .value("value1") + .token("my-own-token") + .tokenGroupNames(Collections.singletonList("group1")) + .build()); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, "vault123"); + + Assert.assertEquals("my-own-token", body.getData().get().get(0).getToken().get()); } // ── createBulkInsertBatches ──────────────────────────────────────────────── @@ -550,21 +574,16 @@ public void testCreateBulkDeleteTokensBatches_splitsTokens() { @Test public void testCreateBulkTokenizeBatches_splitsData() { - com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj1 = - com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build(); - com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj2 = - com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v2").build(); - V1FlowTokenizeRequest request = V1FlowTokenizeRequest.builder() - .vaultId("vault123") - .data(Arrays.asList(obj1, obj2)) - .build(); + List records = Arrays.asList( + BulkTokenizeRequestRecord.builder().value("v1").build(), + BulkTokenizeRequestRecord.builder().value("v2").build()); - List batches = Utils.createBulkTokenizeBatches(request, 1); + List> batches = Utils.createBulkTokenizeBatches(records, 1); Assert.assertEquals(2, batches.size()); - Assert.assertEquals(1, batches.get(0).getData().get().size()); - Assert.assertEquals("v1", batches.get(0).getData().get().get(0).getValue().get()); - Assert.assertEquals("v2", batches.get(1).getData().get().get(0).getValue().get()); + Assert.assertEquals(1, batches.get(0).size()); + Assert.assertEquals("v1", batches.get(0).get(0).getValue()); + Assert.assertEquals("v2", batches.get(1).get(0).getValue()); } // ── createErrorRecord ────────────────────────────────────────────────────── @@ -762,6 +781,37 @@ public void testHandleBulkInsertBatchException_genericExceptionHasNoRequestId() Assert.assertEquals("boom", records.get(0).getError()); } + @Test + public void testHandleBulkInsertBatchException_nonApiCauseUsesCauseMessage() { + // Cause is non-null but not an ApiClientApiException: the message ladder should + // pick up the cause's own message rather than the outer wrapper's. + RuntimeException ex = new RuntimeException("wrapper", new IllegalStateException("inner boom")); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("inner boom", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_nonApiCauseWithNestedCauseUsesNestedToString() { + // When the cause itself wraps another throwable, the ladder resolves the message + // down to the nested cause's toString(). + RuntimeException ex = new RuntimeException("wrapper", + new IllegalStateException("inner boom", new IllegalArgumentException("root cause"))); + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List records = Utils.handleBulkInsertBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError()); + } + // ── createInsertErrorRecord / createDetokenizeErrorRecord branch coverage ─ @Test @@ -799,6 +849,98 @@ public void testCreateErrorRecords_noHttpCodeKeyDefaultsTo500() { Assert.assertEquals(500, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); } + // ── error-record building: keys present with explicit null values ──────── + // Regression: the vault sends "skyflowID": null / "tableName": null on failed records, and + // containsKey() is true for those. Reading them unguarded threw NPE and masked the real error. + + @Test + public void testCreateInsertErrorRecord_nullSkyflowIdAndTableNameDoNotThrow() { + Map recordMap = new HashMap<>(); + recordMap.put("skyflowID", null); + recordMap.put("tableName", null); + recordMap.put("error", "Invalid request. Table not found."); + recordMap.put("httpCode", 400); + + BulkInsertResponseRecord record = Utils.createInsertErrorRecord(recordMap, 0, "req-1"); + + Assert.assertNull(record.getSkyflowId()); + Assert.assertNull(record.getTableName()); + Assert.assertEquals(400, record.getHttpCode()); + Assert.assertEquals("Invalid request. Table not found.", record.getError()); + } + + @Test + public void testCreateDetokenizeErrorRecord_nullTokenFieldsDoNotThrow() { + Map recordMap = new HashMap<>(); + recordMap.put("token", null); + recordMap.put("tokenGroupName", null); + recordMap.put("error", "Token not found."); + recordMap.put("httpCode", 404); + + BulkDetokenizeResponseRecord record = Utils.createDetokenizeErrorRecord(recordMap, 0, "req-1"); + + Assert.assertNull(record.getToken()); + Assert.assertNull(record.getTokenGroupName()); + Assert.assertEquals(404, record.getHttpCode()); + Assert.assertEquals("Token not found.", record.getError()); + } + + @Test + public void testCreateErrorRecords_nullHttpCodeValueFallsBackTo500() { + Map recordMap = new HashMap<>(); + recordMap.put("httpCode", null); + recordMap.put("error", "boom"); + + Assert.assertEquals(500, Utils.createErrorRecord(recordMap, 0, null).getCode()); + Assert.assertEquals(500, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(500, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_nonIntegerHttpCodeIsCoerced() { + Map asLong = new HashMap<>(); + asLong.put("httpCode", 409L); + asLong.put("error", "conflict"); + Assert.assertEquals(409, Utils.createInsertErrorRecord(asLong, 0, null).getHttpCode()); + + Map asDouble = new HashMap<>(); + asDouble.put("httpCode", 503.0d); + asDouble.put("error", "unavailable"); + Assert.assertEquals(503, Utils.createInsertErrorRecord(asDouble, 0, null).getHttpCode()); + + Map asText = new HashMap<>(); + asText.put("httpCode", "422"); + asText.put("error", "unprocessable"); + Assert.assertEquals(422, Utils.createInsertErrorRecord(asText, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_nonStringErrorDoesNotThrow() { + Map nested = new HashMap<>(); + nested.put("detail", "inner"); + Map recordMap = new HashMap<>(); + recordMap.put("error", nested); + recordMap.put("httpCode", 500); + + Assert.assertNotNull(Utils.createInsertErrorRecord(recordMap, 0, null).getError()); + Assert.assertNotNull(Utils.createErrorRecord(recordMap, 0, null).getError()); + } + + @Test + public void testCreateErrorRecords_nullErrorValueFallsThroughToMessage() { + // A null error text would make the record read as a SUCCESS downstream, since failures are + // counted by getError() != null. + Map withMessage = new HashMap<>(); + withMessage.put("error", null); + withMessage.put("message", "vault unreachable"); + Assert.assertEquals("vault unreachable", Utils.createInsertErrorRecord(withMessage, 0, null).getError()); + + Map withNeither = new HashMap<>(); + withNeither.put("error", null); + withNeither.put("message", null); + Assert.assertEquals("Unknown error", Utils.createInsertErrorRecord(withNeither, 0, null).getError()); + } + @Test public void testCreateErrorRecords_messageKeyIsUsedWhenErrorKeyAbsent() { Map recordMap = new HashMap<>(); @@ -1170,6 +1312,24 @@ public void testHandleBulkDetokenizeBatchException_genericException() { Assert.assertEquals("boom", errors.get(0).getError()); } + @Test + public void testHandleBulkDetokenizeBatchException_nonApiCauseUsesCauseMessage() { + // Cause is non-null but not an ApiClientApiException: the message ladder resolves the + // nested cause's toString() rather than the outer wrapper's message. + RuntimeException ex = new RuntimeException("wrapper", + new IllegalStateException("inner boom", new IllegalArgumentException("root cause"))); + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + + List records = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + Assert.assertEquals("java.lang.IllegalArgumentException: root cause", records.get(0).getError()); + } + // ── handleBulkDeleteTokensBatchException ────────────────────────────────── @Test @@ -1186,11 +1346,14 @@ public void testHandleBulkDeleteTokensBatchException_apiExceptionWithTokensBody( .vaultId("vault123") .tokens(Collections.singletonList("t1")) .build(); - List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(404, errors.get(0).getCode()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getHttpCode()); Assert.assertEquals("not found", errors.get(0).getError()); + // token comes from the batch we sent, so an error record is never missing it + Assert.assertEquals("t1", errors.get(0).getToken()); + Assert.assertEquals(0, errors.get(0).getIndex()); } @Test @@ -1204,11 +1367,12 @@ public void testHandleBulkDeleteTokensBatchException_apiExceptionWithErrorKeyBod .vaultId("vault123") .tokens(Collections.singletonList("t1")) .build(); - List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(403, errors.get(0).getCode()); + Assert.assertEquals(Integer.valueOf(403), errors.get(0).getHttpCode()); Assert.assertEquals("top level delete error", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); } @Test @@ -1219,47 +1383,302 @@ public void testHandleBulkDeleteTokensBatchException_genericException() { .tokens(Collections.singletonList("t1")) .build(); - List errors = Utils.handleBulkDeleteTokensBatchException(ex, batch, 0, 50); + List errors = Utils.handleBulkDeleteTokensBatchException(ex, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getHttpCode()); + Assert.assertEquals("t1", errors.get(0).getToken()); } - // ── handleBulkTokenizeBatchException ─────────────────────────────────────── + @Test + public void testHandleBulkDeleteTokensBatchException_errorFieldAsObjectUsesHelper() { + // Structured error envelope {"error": {message, httpCode}} → parsed per token via the helper. + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + errorObject.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getHttpCode()); + Assert.assertEquals("vault not found", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } @Test - public void testHandleBulkTokenizeBatchException_apiExceptionWithResponseBody() { - Map errorRecordMap = new HashMap<>(); - errorRecordMap.put("error", "invalid value"); - errorRecordMap.put("httpCode", 400); + public void testHandleBulkDeleteTokensBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { Map body = new HashMap<>(); - body.put("response", Collections.singletonList(errorRecordMap)); - ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 500, body); RuntimeException wrapper = new RuntimeException(apiEx); - V1FlowTokenizeRequest batch = V1FlowTokenizeRequest.builder() + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() .vaultId("vault123") - .data(Collections.singletonList(com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build())) + .tokens(Collections.singletonList("t1")) .build(); - List errors = Utils.handleBulkTokenizeBatchException(wrapper, batch, 0, 50); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(400, errors.get(0).getCode()); - Assert.assertEquals("invalid value", errors.get(0).getError()); + Assert.assertEquals("delete failed", errors.get(0).getError()); } @Test - public void testHandleBulkTokenizeBatchException_genericException() { - RuntimeException ex = new RuntimeException("boom"); - V1FlowTokenizeRequest batch = V1FlowTokenizeRequest.builder() + public void testHandleBulkDeleteTokensBatchException_bodyWithNeitherTokensNorErrorKey() { + // A map body matching neither branch falls through to the batch-wide fallback. + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 1, 50); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(Integer.valueOf(503), errors.get(0).getHttpCode()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + // startIndex = batchNumber * batchSize = 50 + Assert.assertEquals(50, errors.get(0).getIndex()); + Assert.assertEquals(51, errors.get(1).getIndex()); + Assert.assertEquals("t2", errors.get(1).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_tokensNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("tokens", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("tokens", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + // No entry parsed, so the batch-wide fallback fires instead. + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("delete failed", errors.get(0).getError()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_recordEchoesValueAndReadsHttpCodeAndMessage() { + // createDeleteTokensErrorRecord: http_code key, "message" key, and an echoed "value" token. + Map tokenMap = new HashMap<>(); + tokenMap.put("http_code", 409); + tokenMap.put("message", "already deleted"); + tokenMap.put("value", "echoed-token"); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(tokenMap)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 409, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("requested-token")) + .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(409), errors.get(0).getHttpCode()); + Assert.assertEquals("already deleted", errors.get(0).getError()); + // the echoed "value" wins over the token from the request batch + Assert.assertEquals("echoed-token", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_recordUsesStatusCodeAndUnknownError() { + // createDeleteTokensErrorRecord: statusCode key and the no-error/no-message fallback. + Map tokenMap = new HashMap<>(); + tokenMap.put("statusCode", 410); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(tokenMap)); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 410, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() .vaultId("vault123") - .data(Collections.singletonList(com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build())) + .tokens(Collections.singletonList("t1")) .build(); + List errors = Utils.handleBulkDeleteTokensBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(410), errors.get(0).getHttpCode()); + Assert.assertEquals("Unknown error", errors.get(0).getError()); + // no echoed value, so the requested token is reported + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + // ── handleBulkTokenizeBatchException ─────────────────────────────────────── + + private static List tokenizeBatch(String value, String... groups) { + return Collections.singletonList(BulkTokenizeRequestRecord.builder() + .value(value).tokenGroupNames(Arrays.asList(groups)).build()); + } + + @Test + public void testHandleBulkTokenizeBatchException_apiExceptionFailsEveryTokenGroup() { + Map body = new HashMap<>(); + body.put("error", "invalid value"); + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1", "group2"), 0); + + Assert.assertEquals(1, errors.size()); + BulkTokenizeResponseRecord record = errors.get(0); + // index is derived from the batch position; the value is echoed from the request + Assert.assertEquals(0, record.getIndex()); + Assert.assertEquals("v1", record.getValue()); + // one failed token entry per requested group + Assert.assertEquals(2, record.getTokens().size()); + Assert.assertEquals("group1", record.getTokens().get(0).getTokenGroupName()); + Assert.assertEquals("invalid value", record.getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), record.getTokens().get(0).getHttpCode()); + Assert.assertEquals("group2", record.getTokens().get(1).getTokenGroupName()); + Assert.assertEquals("invalid value", record.getTokens().get(1).getError()); + } + + @Test + public void testHandleBulkTokenizeBatchException_genericException() { + RuntimeException ex = new RuntimeException("boom"); + + // this batch starts at index 3 in the caller's list + List errors = Utils.handleBulkTokenizeBatchException( + ex, tokenizeBatch("v1", "group1"), 3); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(3, errors.get(0).getIndex()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode()); + Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError()); + } + + @Test + public void testHandleBulkTokenizeBatchException_indexesRunConsecutivelyFromBatchStart() { + RuntimeException ex = new RuntimeException("boom"); + List batch = Arrays.asList( + BulkTokenizeRequestRecord.builder().value("v1").build(), + BulkTokenizeRequestRecord.builder().value("v2").build()); + + // this batch starts at index 20, so it covers 20 and 21 + List errors = Utils.handleBulkTokenizeBatchException(ex, batch, 20); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(20, errors.get(0).getIndex()); + Assert.assertEquals("v1", errors.get(0).getValue()); + Assert.assertEquals(21, errors.get(1).getIndex()); + Assert.assertEquals("v2", errors.get(1).getValue()); + } + + @Test + public void testHandleBulkTokenizeBatchException_noTokenGroupsStillReportsOneEntry() { + RuntimeException ex = new RuntimeException("boom"); + List batch = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("v1").build()); + + List errors = Utils.handleBulkTokenizeBatchException(ex, batch, 0); + + Assert.assertEquals(1, errors.get(0).getTokens().size()); + Assert.assertNull(errors.get(0).getTokens().get(0).getTokenGroupName()); + Assert.assertEquals("boom", errors.get(0).getTokens().get(0).getError()); + } + + @Test + public void testHandleBulkTokenizeBatchException_errorBodyWithResponseArrayRebuildsRecords() { + // A 4xx whose body echoes the per-row "response" array is rebuilt via tokenizeRecordsFromErrorBody + // rather than summarized by the bare status code. + Map tokenRow = new HashMap<>(); + tokenRow.put("tokenGroupName", "group1"); + tokenRow.put("error", "BYOT token should contain one token group"); + tokenRow.put("httpCode", 400); + Map responseRow = new HashMap<>(); + responseRow.put("value", "v1"); + responseRow.put("tokens", Collections.singletonList(tokenRow)); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(responseRow)); + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); - List errors = Utils.handleBulkTokenizeBatchException(ex, batch, 0, 50); + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); Assert.assertEquals(1, errors.size()); - Assert.assertEquals(500, errors.get(0).getCode()); + Assert.assertEquals("v1", errors.get(0).getValue()); + Assert.assertEquals(1, errors.get(0).getTokens().size()); + Assert.assertEquals("group1", errors.get(0).getTokens().get(0).getTokenGroupName()); + Assert.assertEquals("BYOT token should contain one token group", + errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(400), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_errorFieldAsObjectUsesStructuredMessage() { + // extractBatchErrorMessage reads {"error": {message}} when the body has no per-row response. + Map errorObject = new HashMap<>(); + errorObject.put("message", "vault not found"); + Map body = new HashMap<>(); + body.put("error", errorObject); + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("vault not found", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(404), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_nonMapBodyUsesApiMessage() { + // Body is not a map, so extractBatchErrorMessage falls back to the exception's own message. + ApiClientApiException apiEx = new ApiClientApiException("tokenize failed", 500, "raw string body"); + RuntimeException wrapper = new RuntimeException(apiEx); + + List errors = Utils.handleBulkTokenizeBatchException( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals("tokenize failed", errors.get(0).getTokens().get(0).getError()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getTokens().get(0).getHttpCode()); + } + + @Test + public void testHandleBulkTokenizeBatchException_nullBatchReturnsEmpty() { + RuntimeException ex = new RuntimeException("boom"); + + List errors = Utils.handleBulkTokenizeBatchException(ex, null, 0); + + Assert.assertTrue(errors.isEmpty()); } // ── formatBulkInsertResponse ─────────────────────────────────────────────── @@ -1378,6 +1797,47 @@ public void testFormatBulkDetokenizeResponse_emptyResponseReturnsNull() { // ── formatBulkDeleteTokensResponse ───────────────────────────────────────── + private static V1FlowDeleteTokenRequest deleteBatchOf(String... tokens) { + return V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList(tokens)) + .build(); + } + + @Test + public void testFormatBulkDeleteTokensResponse_duplicateTokenRelaysEachRowVerbatim() { + // the same token sent twice: the API decides each position independently, and has been + // observed returning both 200,200 and 200,404 for the identical request. Whatever it says + // must reach the caller unchanged - no deduplication, no normalising one row against the other. + String token = "e5874be2-940a-4c74-9c08-dc6c1e8c6f9b"; + String message = "DeleteToken failed. Token " + token + " is invalid. Specify a valid token."; + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Arrays.asList( + V1DeleteTokenResponseObject.builder().value(token).httpCode(200).build(), + V1DeleteTokenResponseObject.builder() + .value(token).error(message).httpCode(404).build())) + .build(); + + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf(token, token), 0, 50, new HashMap<>()); + BulkDeleteTokensResponse withPayload = new BulkDeleteTokensResponse( + result.getRecords(), Arrays.asList(token, token)); + + Assert.assertEquals(2, withPayload.getRecords().size()); + Assert.assertEquals(0, withPayload.getRecords().get(0).getIndex()); + Assert.assertEquals(Integer.valueOf(200), withPayload.getRecords().get(0).getHttpCode()); + Assert.assertNull(withPayload.getRecords().get(0).getError()); + Assert.assertEquals(1, withPayload.getRecords().get(1).getIndex()); + Assert.assertEquals(Integer.valueOf(404), withPayload.getRecords().get(1).getHttpCode()); + Assert.assertEquals(message, withPayload.getRecords().get(1).getError()); + // the summary follows the rows, so a duplicate that the API rejected is not counted deleted + Assert.assertEquals(2, withPayload.getSummary().getTotalTokens()); + Assert.assertEquals(1, withPayload.getSummary().getTotalDeleted()); + Assert.assertEquals(1, withPayload.getSummary().getTotalFailed()); + // 404 is not retryable, so nothing is offered for resubmission + Assert.assertTrue(withPayload.getTokensToRetry().isEmpty()); + } + @Test public void testFormatBulkDeleteTokensResponse_success() { V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() @@ -1387,11 +1847,14 @@ public void testFormatBulkDeleteTokensResponse_success() { .tokens(Collections.singletonList(record)) .build(); - BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse(response, 0, 50, new HashMap<>()); + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getSuccess().size()); - Assert.assertEquals("token1", result.getSuccess().get(0).getToken()); - Assert.assertTrue(result.getErrors().isEmpty()); + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals("token1", result.getRecords().get(0).getToken()); + Assert.assertEquals(Integer.valueOf(200), result.getRecords().get(0).getHttpCode()); + Assert.assertNull(result.getRecords().get(0).getError()); + Assert.assertEquals(0, result.getRecords().get(0).getIndex()); } @Test @@ -1404,10 +1867,14 @@ public void testFormatBulkDeleteTokensResponse_error() { .tokens(Collections.singletonList(record)) .build(); - BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse(response, 0, 50, new HashMap<>()); + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); - Assert.assertEquals(1, result.getErrors().size()); - Assert.assertEquals(404, result.getErrors().get(0).getCode()); + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(Integer.valueOf(404), result.getRecords().get(0).getHttpCode()); + Assert.assertEquals("token not found", result.getRecords().get(0).getError()); + // API omitted the echoed value, so the token falls back to the one we sent + Assert.assertEquals("token1", result.getRecords().get(0).getToken()); } @Test @@ -1420,77 +1887,99 @@ public void testFormatBulkDeleteTokensResponse_errorTextWithoutHttpCodeTreatedAs .tokens(Collections.singletonList(record)) .build(); - BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse(response, 0, 50, new HashMap<>()); + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); - Assert.assertTrue(result.getErrors().isEmpty()); - Assert.assertEquals(1, result.getSuccess().size()); - Assert.assertEquals("token1", result.getSuccess().get(0).getToken()); + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertNull(result.getRecords().get(0).getError()); + Assert.assertEquals("token1", result.getRecords().get(0).getToken()); + } + + @Test + public void testFormatBulkDeleteTokensResponse_indexesOffsetByBatch() { + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Arrays.asList( + V1DeleteTokenResponseObject.builder().value("token3").build(), + V1DeleteTokenResponseObject.builder().value("token4").build())) + .build(); + + // batch 1 with batchSize 2 => indexes continue at 2 + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token3", "token4"), 1, 2, new HashMap<>()); + + Assert.assertEquals(2, result.getRecords().get(0).getIndex()); + Assert.assertEquals(3, result.getRecords().get(1).getIndex()); } @Test public void testFormatBulkDeleteTokensResponse_emptyResponseReturnsNull() { V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder().build(); - Assert.assertNull(Utils.formatBulkDeleteTokensResponse(response, 0, 50, new HashMap<>())); + Assert.assertNull(Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>())); } // ── formatBulkTokenizeResponse ───────────────────────────────────────────── + private static V1FlowTokenizeResponse tokenizeWire(V1FlowTokenizeResponseObject... records) { + return V1FlowTokenizeResponse.builder().response(java.util.Arrays.asList(records)).build(); + } + @Test public void testFormatBulkTokenizeResponse_success() { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .token("tok-abc") - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() + V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse response = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - V1FlowTokenizeRequest batchRequest = V1FlowTokenizeRequest.builder() - .vaultId("vault123") - .data(Collections.singletonList(com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("value1").build())) - .build(); + .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").token("tok-abc").build())) + .build()); - BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(response, batchRequest, 0, 50, new HashMap<>()); + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); - Assert.assertEquals(1, result.getSuccess().size()); - Assert.assertEquals("tok-abc", result.getSuccess().get(0).getTokens().get("group1")); - Assert.assertTrue(result.getErrors().isEmpty()); + Assert.assertEquals(1, result.getRecords().size()); + BulkTokenizeResponseRecord record = result.getRecords().get(0); + Assert.assertEquals(0, record.getIndex()); + Assert.assertEquals("value1", record.getValue()); + Assert.assertEquals("tok-abc", record.getTokens().get(0).getToken()); + Assert.assertNull(record.getTokens().get(0).getError()); } @Test public void testFormatBulkTokenizeResponse_tokenError() { - FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() - .tokenGroupName("group1") - .error("invalid value") - .httpCode(400) - .build(); - V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() + V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() .value("value1") - .tokens(Collections.singletonList(token)) - .build(); - V1FlowTokenizeResponse response = V1FlowTokenizeResponse.builder() - .response(Collections.singletonList(responseObject)) - .build(); - V1FlowTokenizeRequest batchRequest = V1FlowTokenizeRequest.builder() - .vaultId("vault123") - .data(Collections.singletonList(com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("value1").build())) - .build(); + .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").error("invalid value").httpCode(400).build())) + .build()); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + TokenizeResponseToken token = result.getRecords().get(0).getTokens().get(0); + Assert.assertEquals(Integer.valueOf(400), token.getHttpCode()); + Assert.assertEquals("invalid value", token.getError()); + } + + @Test + public void testFormatBulkTokenizeResponse_derivesIndexFromBatchPosition() { + V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() + .value("value1") + .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").token("tok-abc").build())) + .build()); - BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse(response, batchRequest, 0, 50, new HashMap<>()); + // this batch starts at index 40 in the caller's list + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + response, tokenizeBatch("value1", "group1"), 40, new HashMap<>()); - Assert.assertTrue(result.getSuccess().isEmpty()); - Assert.assertEquals(1, result.getErrors().size()); - Assert.assertEquals(400, result.getErrors().get(0).getCode()); + Assert.assertEquals(40, result.getRecords().get(0).getIndex()); } @Test public void testFormatBulkTokenizeResponse_emptyResponseReturnsNull() { - V1FlowTokenizeResponse response = V1FlowTokenizeResponse.builder().build(); - V1FlowTokenizeRequest batchRequest = V1FlowTokenizeRequest.builder().vaultId("vault123").build(); - Assert.assertNull(Utils.formatBulkTokenizeResponse(response, batchRequest, 0, 50, new HashMap<>())); + Assert.assertNull(Utils.formatBulkTokenizeResponse( + V1FlowTokenizeResponse.builder().build(), + tokenizeBatch("value1", "group1"), 0, new HashMap<>())); } // Tests for getQueryRequestBody / buildQueryResponse / getGetRequestBody / buildGetResponse 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 c9e8dacd..44a58686 100644 --- a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java +++ b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -9,12 +9,14 @@ import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkDetokenizeRequest; -import com.skyflow.vault.data.BulkTokenizeRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.DetokenizeRequest; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.TokenizeRequestRecord; +import com.skyflow.vault.data.TokenizeRequest; import com.skyflow.vault.data.UpsertOptions; import org.junit.Assert; import org.junit.Test; @@ -974,14 +976,14 @@ public void testValidateBulkDeleteTokensRequest_over10000TokensThrows() { @Test public void testValidateBulkTokenizeRequest_over10000RecordsThrows() { - ArrayList records = new ArrayList<>(); + ArrayList records = new ArrayList<>(); for (int i = 0; i < 10001; i++) { - records.add(BulkTokenizeRecord.builder() + records.add(BulkTokenizeRequestRecord.builder() .value("value-" + i) .tokenGroupNames(Collections.singletonList("group")) .build()); } - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); try { Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -1435,7 +1437,7 @@ public void testValidateBulkTokenizeRequest_nullRequest() { @Test public void testValidateBulkTokenizeRequest_emptyData() { - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(new ArrayList<>()).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(new ArrayList<>()).build(); try { Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -1446,9 +1448,9 @@ public void testValidateBulkTokenizeRequest_emptyData() { @Test public void testValidateBulkTokenizeRequest_nullRecordInList() { - ArrayList data = new ArrayList<>(); + ArrayList data = new ArrayList<>(); data.add(null); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); try { Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -1459,9 +1461,9 @@ public void testValidateBulkTokenizeRequest_nullRecordInList() { @Test public void testValidateBulkTokenizeRequest_emptyValue() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value(" ").build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRequestRecord.builder().value(" ").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); try { Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -1472,9 +1474,9 @@ public void testValidateBulkTokenizeRequest_emptyValue() { @Test public void testValidateBulkTokenizeRequest_emptyGroupNameInList() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Arrays.asList("group1", " ")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRequestRecord.builder().value("value1").tokenGroupNames(Arrays.asList("group1", " ")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); try { Validations.validateBulkTokenizeRequest(request); Assert.fail(EXCEPTION_NOT_THROWN); @@ -1485,9 +1487,9 @@ public void testValidateBulkTokenizeRequest_emptyGroupNameInList() { @Test public void testValidateBulkTokenizeRequest_validRequestWithTokenGroupNames() { - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRequestRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); try { Validations.validateBulkTokenizeRequest(request); } catch (SkyflowException e) { @@ -1498,9 +1500,23 @@ public void testValidateBulkTokenizeRequest_validRequestWithTokenGroupNames() { @Test public void testValidateBulkTokenizeRequest_validRequestWithoutTokenGroupNames() { // Unlike the non-bulk tokenize validator, tokenGroupNames is optional for bulk requests. - ArrayList data = new ArrayList<>(); - data.add(BulkTokenizeRecord.builder().value("value1").build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRequestRecord.builder().value("value1").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); + try { + Validations.validateBulkTokenizeRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkTokenizeRequest_nonSequentialIndexesAccepted() { + // indexes only need to be present and unique - gaps and ordering are the caller's business + ArrayList data = new ArrayList<>(); + data.add(BulkTokenizeRequestRecord.builder().value("value1").build()); + data.add(BulkTokenizeRequestRecord.builder().value("value2").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); try { Validations.validateBulkTokenizeRequest(request); } catch (SkyflowException e) { diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/BatchConfigResolutionTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/BatchConfigResolutionTests.java new file mode 100644 index 00000000..e75b9e1e --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/controller/BatchConfigResolutionTests.java @@ -0,0 +1,264 @@ +package com.skyflow.vault.controller; + +import com.skyflow.VaultClient; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.utils.Constants; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Batch size and concurrency resolution for all four bulk operations. + * + *

These settings are user-tunable only through the environment, so the tests swap + * {@link VaultController#settingResolver} rather than mutating the JVM environment. Each operation + * has its own near-identical resolver, so every case runs against all four to stop one drifting. + */ +public class BatchConfigResolutionTests { + + private final Function originalResolver = VaultController.settingResolver; + + @After + public void restoreResolver() { + VaultController.settingResolver = originalResolver; + } + + /** Operation name, its env-var prefix, and its default/max constants. */ + private static final String[][] OPS = { + {"Insert", "INSERT"}, + {"Detokenize", "DETOKENIZE"}, + {"Tokenize", "TOKENIZE"}, + {"DeleteTokens", "DELETE_TOKENS"}, + }; + + private static void useSettings(Map settings) { + VaultController.settingResolver = settings::get; + } + + private static Map settings(String prefix, String batchSize, String concurrency) { + Map map = new HashMap<>(); + if (batchSize != null) { + map.put(prefix + "_BATCH_SIZE", batchSize); + } + if (concurrency != null) { + map.put(prefix + "_CONCURRENCY_LIMIT", concurrency); + } + return map; + } + + private static VaultController controller() throws SkyflowException { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault1"); + config.setClusterId("cluster1"); + config.setEnv(Env.DEV); + return new VaultController(config, null); + } + + /** Invokes the private configure<Op>ConcurrencyAndBatchSize and reads the result. */ + private static int[] resolve(String op, int totalRequests) throws Exception { + Method method = VaultController.class.getDeclaredMethod( + "configure" + op + "ConcurrencyAndBatchSize", int.class); + method.setAccessible(true); + Object cfg; + try { + cfg = method.invoke(controller(), totalRequests); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } + Field batchSize = cfg.getClass().getDeclaredField("batchSize"); + Field concurrency = cfg.getClass().getDeclaredField("concurrencyLimit"); + batchSize.setAccessible(true); + concurrency.setAccessible(true); + return new int[] {(int) batchSize.get(cfg), (int) concurrency.get(cfg)}; + } + + private static int defaultBatchSize(String prefix) { + switch (prefix) { + case "INSERT": return Constants.INSERT_BATCH_SIZE; + case "DETOKENIZE": return Constants.DETOKENIZE_BATCH_SIZE; + case "TOKENIZE": return Constants.TOKENIZE_BATCH_SIZE; + default: return Constants.DELETE_TOKENS_BATCH_SIZE; + } + } + + private static int maxBatchSize(String prefix) { + switch (prefix) { + case "INSERT": return Constants.MAX_INSERT_BATCH_SIZE; + case "DETOKENIZE": return Constants.MAX_DETOKENIZE_BATCH_SIZE; + case "TOKENIZE": return Constants.MAX_TOKENIZE_BATCH_SIZE; + default: return Constants.MAX_DELETE_TOKENS_BATCH_SIZE; + } + } + + private static int maxConcurrency(String prefix) { + switch (prefix) { + case "INSERT": return Constants.MAX_INSERT_CONCURRENCY_LIMIT; + case "DETOKENIZE": return Constants.MAX_DETOKENIZE_CONCURRENCY_LIMIT; + case "TOKENIZE": return Constants.MAX_TOKENIZE_CONCURRENCY_LIMIT; + default: return Constants.MAX_DELETE_TOKENS_CONCURRENCY_LIMIT; + } + } + + // ── batch size ──────────────────────────────────────────────────────────── + + @Test + public void testBatchSize_defaultsWhenNothingIsConfigured() throws Exception { + for (String[] op : OPS) { + useSettings(new HashMap<>()); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_honoursAValidSetting() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "200", null)); + Assert.assertEquals(op[0], 200, resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_isCappedAtTheMaximum() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "999999", null)); + Assert.assertEquals(op[0], maxBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_exactlyAtTheMaximumIsAccepted() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], String.valueOf(maxBatchSize(op[1])), null)); + Assert.assertEquals(op[0], maxBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_zeroFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "0", null)); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_negativeFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "-5", null)); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_nonNumericFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "not-a-number", null)); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + @Test + public void testBatchSize_emptyStringFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "", null)); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), resolve(op[0], 10_000)[0]); + } + } + + // ── concurrency limit ───────────────────────────────────────────────────── + + @Test + public void testConcurrency_defaultsToOneWhenNothingIsConfigured() throws Exception { + for (String[] op : OPS) { + useSettings(new HashMap<>()); + Assert.assertEquals(op[0], 1, resolve(op[0], 10_000)[1]); + } + } + + @Test + public void testConcurrency_honoursAValidSetting() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "1", "5")); + Assert.assertEquals(op[0], 5, resolve(op[0], 10_000)[1]); + } + } + + @Test + public void testConcurrency_isCappedAtTheMaximum() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "1", "999")); + Assert.assertEquals(op[0], maxConcurrency(op[1]), resolve(op[0], 10_000)[1]); + } + } + + @Test + public void testConcurrency_zeroFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "1", "0")); + Assert.assertEquals(op[0], 1, resolve(op[0], 10_000)[1]); + } + } + + @Test + public void testConcurrency_negativeFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "1", "-3")); + Assert.assertEquals(op[0], 1, resolve(op[0], 10_000)[1]); + } + } + + @Test + public void testConcurrency_nonNumericFallsBackToTheDefault() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "1", "lots")); + Assert.assertEquals(op[0], 1, resolve(op[0], 10_000)[1]); + } + } + + // ── concurrency is further capped by how many batches there actually are ── + + @Test + public void testConcurrency_neverExceedsTheNumberOfBatches() throws Exception { + for (String[] op : OPS) { + // 25 records at batch size 10 is 3 batches, so 10 threads would leave 7 idle. + useSettings(settings(op[1], "10", "10")); + Assert.assertEquals(op[0], 3, resolve(op[0], 25)[1]); + } + } + + @Test + public void testConcurrency_singleBatchUsesASingleThread() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "100", "10")); + Assert.assertEquals(op[0], 1, resolve(op[0], 5)[1]); + } + } + + @Test + public void testConcurrency_exactBatchMultipleDoesNotRoundUp() throws Exception { + for (String[] op : OPS) { + useSettings(settings(op[1], "10", "10")); + Assert.assertEquals(op[0], 2, resolve(op[0], 20)[1]); + } + } + + @Test + public void testBatchSizeAndConcurrency_resolveIndependently() throws Exception { + for (String[] op : OPS) { + // batch size invalid (falls back), concurrency valid + useSettings(settings(op[1], "oops", "4")); + int[] cfg = resolve(op[0], 10_000); + Assert.assertEquals(op[0], defaultBatchSize(op[1]), cfg[0]); + Assert.assertEquals(op[0], 4, cfg[1]); + } + } +} diff --git a/flowvault/src/test/java/com/skyflow/vault/controller/BulkDeleteTokensBatchingTests.java b/flowvault/src/test/java/com/skyflow/vault/controller/BulkDeleteTokensBatchingTests.java new file mode 100644 index 00000000..66bb20a0 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/controller/BulkDeleteTokensBatchingTests.java @@ -0,0 +1,235 @@ +package com.skyflow.vault.controller; + +import com.skyflow.VaultClient; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ApiClientHttpResponse; +import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; +import com.skyflow.generated.rest.resources.flowservice.RawFlowserviceClient; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.utils.Constants; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * Batching and concurrency behaviour for bulkDeleteTokens / bulkDeleteTokensAsync. + * + *

Batch size and concurrency are user-tunable only through the environment, so these tests swap + * {@link VaultController#settingResolver} instead of mutating the JVM environment. Every case here + * drives more than one batch and forces batches to complete out of order, so an implementation that + * derived the record index from completion order (rather than from the batch's position in the + * original request) would fail. + */ +public class BulkDeleteTokensBatchingTests { + + private static final int TOTAL_TOKENS = 50; + + private final Function originalResolver = VaultController.settingResolver; + + @After + public void restoreResolver() { + VaultController.settingResolver = originalResolver; + } + + private static void useBatching(int batchSize, int concurrency) { + Map settings = new HashMap<>(); + settings.put("DELETE_TOKENS_BATCH_SIZE", String.valueOf(batchSize)); + settings.put("DELETE_TOKENS_CONCURRENCY_LIMIT", String.valueOf(concurrency)); + VaultController.settingResolver = settings::get; + } + + private static Response okHttp() { + return new Response.Builder() + .request(new Request.Builder().url("https://dummy.example.com").build()) + .protocol(Protocol.HTTP_1_1).code(200).message("OK") + .header(Constants.REQUEST_ID_HEADER_KEY, "req-test-123").build(); + } + + /** Delay derived from the batch's first token so batches finish in a scrambled order. */ + private static void scrambleCompletion(List tokens) throws InterruptedException { + int first = Integer.parseInt(tokens.get(0).substring(4)); + Thread.sleep(((first * 7919L) % 41) + 3); + } + + private static List tokens(int count) { + List tokens = new ArrayList<>(); + for (int i = 0; i < count; i++) { + tokens.add("tok-" + i); + } + return tokens; + } + + private static VaultController controllerWith(ApiClient mockApi) throws Exception { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster123"); + config.setEnv(Env.DEV); + VaultController controller = new VaultController(config, creds); + Field field = VaultClient.class.getDeclaredField("apiClient"); + field.setAccessible(true); + field.set(controller, mockApi); + return controller; + } + + private static RawFlowserviceClient mockRawFlowservice(ApiClient mockApi) { + FlowserviceClient mockFlow = Mockito.mock(FlowserviceClient.class); + RawFlowserviceClient mockRaw = Mockito.mock(RawFlowserviceClient.class); + when(mockApi.flowservice()).thenReturn(mockFlow); + when(mockFlow.withRawResponse()).thenReturn(mockRaw); + return mockRaw; + } + + /** Echoes each batch back; any token whose number ends in 7 fails with 404. */ + private static ApiClient mockApiEchoingBatches() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.deletetoken(any(), any())).thenAnswer(invocation -> { + V1FlowDeleteTokenRequest request = invocation.getArgument(0); + List batchTokens = request.getTokens().get(); + scrambleCompletion(batchTokens); + List records = new ArrayList<>(); + for (String token : batchTokens) { + if (token.endsWith("7")) { + records.add(V1DeleteTokenResponseObject.builder() + .value(token).error("Token not found").httpCode(404).build()); + } else { + records.add(V1DeleteTokenResponseObject.builder() + .value(token).httpCode(200).build()); + } + } + return new ApiClientHttpResponse<>( + V1FlowDeleteTokenResponse.builder().tokens(records).build(), okHttp()); + }); + return mockApi; + } + + private static void assertIndexesPreserved(BulkDeleteTokensResponse response) { + Assert.assertEquals(TOTAL_TOKENS, response.getRecords().size()); + for (int i = 0; i < TOTAL_TOKENS; i++) { + BulkDeleteTokensResponseRecord record = response.getRecords().get(i); + Assert.assertEquals("index at position " + i, i, record.getIndex()); + Assert.assertEquals("token at position " + i, "tok-" + i, record.getToken()); + } + } + + @Test + public void testBulkDeleteTokens_parallelBatchesPreserveIndexOrder() throws Exception { + useBatching(10, 5); + VaultController controller = controllerWith(mockApiEchoingBatches()); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens( + BulkDeleteTokensRequest.builder().tokens(tokens(TOTAL_TOKENS)).build()); + + assertIndexesPreserved(response); + // tok-7, 17, 27, 37, 47 fail; every failure keeps its own index and token + Assert.assertEquals(TOTAL_TOKENS, response.getSummary().getTotalTokens()); + Assert.assertEquals(5, response.getSummary().getTotalFailed()); + Assert.assertEquals(45, response.getSummary().getTotalDeleted()); + Assert.assertEquals("Token not found", response.getRecords().get(7).getError()); + Assert.assertEquals(Integer.valueOf(404), response.getRecords().get(7).getHttpCode()); + Assert.assertNull(response.getRecords().get(8).getError()); + } + + @Test + public void testBulkDeleteTokensAsync_parallelBatchesPreserveIndexOrder() throws Exception { + useBatching(10, 5); + VaultController controller = controllerWith(mockApiEchoingBatches()); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokensAsync( + BulkDeleteTokensRequest.builder().tokens(tokens(TOTAL_TOKENS)).build()) + .get(30, TimeUnit.SECONDS); + + assertIndexesPreserved(response); + Assert.assertEquals(5, response.getSummary().getTotalFailed()); + Assert.assertEquals(45, response.getSummary().getTotalDeleted()); + } + + @Test + public void testBulkDeleteTokens_maxConcurrencyStillPreservesIndexOrder() throws Exception { + // batch size 5 => 10 batches, all runnable at once at the max concurrency limit + useBatching(5, 10); + VaultController controller = controllerWith(mockApiEchoingBatches()); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens( + BulkDeleteTokensRequest.builder().tokens(tokens(TOTAL_TOKENS)).build()); + + assertIndexesPreserved(response); + Assert.assertEquals(TOTAL_TOKENS, response.getSummary().getTotalTokens()); + } + + @Test + public void testBulkDeleteTokens_failedBatchKeepsItsOwnIndexSlice() throws Exception { + useBatching(10, 5); + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + // the batch starting at tok-20 fails wholesale; the rest succeed + when(mockRaw.deletetoken(any(), any())).thenAnswer(invocation -> { + V1FlowDeleteTokenRequest request = invocation.getArgument(0); + List batchTokens = request.getTokens().get(); + scrambleCompletion(batchTokens); + if ("tok-20".equals(batchTokens.get(0))) { + throw new ApiClientApiException("delete failed", 503, "service unavailable"); + } + List records = new ArrayList<>(); + for (String token : batchTokens) { + records.add(V1DeleteTokenResponseObject.builder().value(token).httpCode(200).build()); + } + return new ApiClientHttpResponse<>( + V1FlowDeleteTokenResponse.builder().tokens(records).build(), okHttp()); + }); + VaultController controller = controllerWith(mockApi); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens( + BulkDeleteTokensRequest.builder().tokens(tokens(TOTAL_TOKENS)).build()); + + // no gaps and no shifting: the failed batch still contributes exactly indexes 20..29 + assertIndexesPreserved(response); + Assert.assertEquals(10, response.getSummary().getTotalFailed()); + Assert.assertEquals(40, response.getSummary().getTotalDeleted()); + for (int i = 20; i < 30; i++) { + Assert.assertNotNull("expected error at index " + i, response.getRecords().get(i).getError()); + Assert.assertEquals(Integer.valueOf(503), response.getRecords().get(i).getHttpCode()); + Assert.assertEquals("tok-" + i, response.getRecords().get(i).getToken()); + } + Assert.assertNull(response.getRecords().get(19).getError()); + Assert.assertNull(response.getRecords().get(30).getError()); + } + + @Test + public void testBulkDeleteTokens_batchSizeAboveMaxIsCapped() throws Exception { + // 5000 exceeds MAX_DELETE_TOKENS_BATCH_SIZE (1000) — capped, so 50 tokens stay one batch + useBatching(5000, 1); + VaultController controller = controllerWith(mockApiEchoingBatches()); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens( + BulkDeleteTokensRequest.builder().tokens(tokens(TOTAL_TOKENS)).build()); + + assertIndexesPreserved(response); + } +} 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 9d96df49..f23fd2d2 100644 --- a/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -22,6 +22,8 @@ import com.skyflow.generated.rest.types.V1InsertResponse; import com.skyflow.generated.rest.types.V1RecordResponseObject; import com.skyflow.utils.Constants; +import com.skyflow.vault.data.BulkDeleteTokensOptions; +import com.skyflow.vault.data.BulkTokenizeOptions; import com.skyflow.vault.data.BulkDeleteTokensRequest; import com.skyflow.vault.data.BulkDeleteTokensResponse; import com.skyflow.vault.data.BulkDetokenizeRequest; @@ -29,17 +31,21 @@ import com.skyflow.vault.data.BulkInsertRequestRecord; import com.skyflow.vault.data.BulkInsertRequest; import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; import com.skyflow.vault.data.BulkInsertResponseRecord; -import com.skyflow.vault.data.BulkTokenizeRecord; import com.skyflow.vault.data.BulkTokenizeRequest; import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeOptions; +import com.skyflow.vault.data.BulkInsertOptions; import com.skyflow.vault.data.DeleteTokensOptions; -import com.skyflow.vault.data.DetokenizeOptions; -import com.skyflow.vault.data.InsertOptions; import com.skyflow.vault.data.InsertRequestRecord; import com.skyflow.vault.data.RequestInterceptor; import com.skyflow.vault.data.TokenGroupRedactions; import com.skyflow.vault.data.TokenizeOptions; +import com.skyflow.vault.data.TokenizeRequestRecord; +import com.skyflow.vault.data.TokenizeRequest; +import com.skyflow.vault.data.TokenizeResponse; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; @@ -192,7 +198,7 @@ public void testBulkInsert_interceptorAddsCustomHeader() throws Exception { BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SkyflowAccountId, "acct-123"); - InsertOptions options = InsertOptions.builder().interceptor(interceptor).build(); + BulkInsertOptions options = BulkInsertOptions.builder().interceptor(interceptor).build(); controller.bulkInsert(request, options); @@ -306,9 +312,10 @@ public void testBulkDeleteTokens_success() throws Exception { BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("token1", response.getSuccess().get(0).getToken()); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("token1", response.getRecords().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getError()); + Assert.assertEquals(Integer.valueOf(200), response.getRecords().get(0).getHttpCode()); } @Test @@ -342,7 +349,8 @@ public void testBulkDeleteTokensAsync_success() throws Exception { BulkDeleteTokensResponse response = controller.bulkDeleteTokensAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getError()); } // ── bulkTokenize ────────────────────────────────────────────────────────── @@ -363,15 +371,17 @@ public void testBulkTokenize_success() throws Exception { VaultController controller = createControllerWithMock(mockApi); - ArrayList records = new ArrayList<>(); - records.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("value1") + .tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); BulkTokenizeResponse response = controller.bulkTokenize(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); - Assert.assertEquals("tok-abc", response.getSuccess().get(0).getTokens().get("group1")); - Assert.assertTrue(response.getErrors().isEmpty()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(0, response.getRecords().get(0).getIndex()); + Assert.assertEquals("tok-abc", response.getRecords().get(0).getTokens().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getTokens().get(0).getError()); } @Test @@ -402,13 +412,15 @@ public void testBulkTokenizeAsync_success() throws Exception { VaultController controller = createControllerWithMock(mockApi); - ArrayList records = new ArrayList<>(); - records.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("value1") + .tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); BulkTokenizeResponse response = controller.bulkTokenizeAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertEquals(1, response.getSuccess().size()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getTokens().get(0).getError()); } // ── additional bulk API-error coverage ─────────────────────────────────── @@ -459,9 +471,10 @@ public void testBulkDeleteTokens_apiErrorCapturedInErrors() throws Exception { BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(404, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(Integer.valueOf(404), response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertEquals("token1", response.getRecords().get(0).getToken()); } @Test @@ -479,9 +492,10 @@ public void testBulkDeleteTokensAsync_apiErrorCapturedInErrors() throws Exceptio BulkDeleteTokensResponse response = controller.bulkDeleteTokensAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(404, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(Integer.valueOf(404), response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertEquals("token1", response.getRecords().get(0).getToken()); } @Test @@ -493,15 +507,18 @@ public void testBulkTokenize_apiErrorCapturedInErrors() throws Exception { VaultController controller = createControllerWithMock(mockApi); - ArrayList records = new ArrayList<>(); - records.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("value1") + .tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); BulkTokenizeResponse response = controller.bulkTokenize(request); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(400, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(0, response.getRecords().get(0).getIndex()); + Assert.assertEquals(Integer.valueOf(400), + response.getRecords().get(0).getTokens().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getTokens().get(0).getError()); } @Test @@ -513,15 +530,18 @@ public void testBulkTokenizeAsync_apiErrorCapturedInErrors() throws Exception { VaultController controller = createControllerWithMock(mockApi); - ArrayList records = new ArrayList<>(); - records.add(BulkTokenizeRecord.builder().value("value1").tokenGroupNames(Collections.singletonList("group1")).build()); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(records).build(); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("value1") + .tokenGroupNames(Collections.singletonList("group1")).build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); BulkTokenizeResponse response = controller.bulkTokenizeAsync(request).get(5, TimeUnit.SECONDS); Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); - Assert.assertTrue(response.getSuccess().isEmpty()); - Assert.assertEquals(1, response.getErrors().size()); - Assert.assertEquals(400, response.getErrors().get(0).getCode()); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(0, response.getRecords().get(0).getIndex()); + Assert.assertEquals(Integer.valueOf(400), + response.getRecords().get(0).getTokens().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getTokens().get(0).getError()); } @Test @@ -868,7 +888,8 @@ public void testBulkInsert_tableNameAndVaultIdReAppliedOnEveryBatch() throws Exc Assert.assertEquals("cards", sent.getTableName().get()); Assert.assertEquals("vault123", sent.getVaultId().get()); for (com.skyflow.generated.rest.types.V1InsertRecordData record : sent.getRecords().get()) { - Assert.assertEquals("cards", record.getTableName().get()); + // The name rides the envelope only — duplicating it per record is rejected by the vault. + Assert.assertFalse(record.getTableName().isPresent()); Assert.assertEquals(String.valueOf(expectedPos), record.getData().get().get("pos")); expectedPos++; } @@ -912,7 +933,7 @@ public void testBulkInsert_interceptorInvokedOncePerBatchWithDistinctContext() t .build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkInsert(request, InsertOptions.builder().interceptor(interceptor).build()); + controller.bulkInsert(request, BulkInsertOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(any(), captor.capture()); @@ -981,7 +1002,7 @@ public void testBulkDetokenize_interceptorInvokedOncePerBatchWithDistinctContext BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(multiBatchTokens()).build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkDetokenize(request, DetokenizeOptions.builder().interceptor(interceptor).build()); + controller.bulkDetokenize(request, BulkDetokenizeOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(any(), captor.capture()); @@ -1013,10 +1034,10 @@ public void testBulkDeleteTokens_vaultIdOnEveryBatchAndTokenOrderPreserved() thr } Assert.assertEquals(tokens, flattened); - Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getSuccess().size()); + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { - Assert.assertEquals(i, response.getSuccess().get(i).getIndex()); - Assert.assertEquals(tokens.get(i), response.getSuccess().get(i).getToken()); + Assert.assertEquals(i, response.getRecords().get(i).getIndex()); + Assert.assertEquals(tokens.get(i), response.getRecords().get(i).getToken()); } } @@ -1030,7 +1051,7 @@ public void testBulkDeleteTokens_interceptorInvokedOncePerBatchWithDistinctConte BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(multiBatchTokens()).build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkDeleteTokens(request, DeleteTokensOptions.builder().interceptor(interceptor).build()); + controller.bulkDeleteTokens(request, BulkDeleteTokensOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).deletetoken(any(), captor.capture()); @@ -1046,14 +1067,14 @@ public void testBulkTokenize_vaultIdOnEveryBatchAndValueOrderPreserved() throws stubTokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - ArrayList data = new ArrayList<>(); + ArrayList records = new ArrayList<>(); for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { - data.add(BulkTokenizeRecord.builder() + records.add(BulkTokenizeRequestRecord.builder() .value("value-" + i) .tokenGroupNames(Collections.singletonList("group1")) .build()); } - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); controller.bulkTokenize(request); @@ -1082,14 +1103,14 @@ public void testBulkTokenize_interceptorInvokedOncePerBatchWithDistinctContext() stubTokenizeEcho(mockRaw); VaultController controller = createControllerWithMock(mockApi); - ArrayList data = new ArrayList<>(); + ArrayList records = new ArrayList<>(); for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { - data.add(BulkTokenizeRecord.builder().value("value-" + i).build()); + records.add(BulkTokenizeRequestRecord.builder().value("value-" + i).build()); } - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); CountingInterceptor interceptor = new CountingInterceptor(); - controller.bulkTokenize(request, TokenizeOptions.builder().interceptor(interceptor).build()); + controller.bulkTokenize(request, BulkTokenizeOptions.builder().interceptor(interceptor).build()); ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).tokenize(any(), captor.capture()); 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 d765345e..fc6b4e65 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -254,71 +254,144 @@ public void testBulkDetokenizeResponse_recordsExposeDetokenizedValue() { // ── BulkDeleteTokensResponse ───────────────────────────────────────────── @Test - public void testBulkDeleteTokensResponse_twoArgConstructorLeavesSummaryNull() { - List success = Collections.emptyList(); - List errors = Collections.emptyList(); + public void testBulkDeleteTokensResponse_singleArgConstructorLeavesSummaryNull() { + List records = Collections.emptyList(); - BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(success, errors); + BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(records); Assert.assertNull(response.getSummary()); - Assert.assertEquals(success, response.getSuccess()); - Assert.assertEquals(errors, response.getErrors()); + Assert.assertEquals(records, response.getRecords()); } @Test - public void testBulkDeleteTokensResponse_threeArgConstructorComputesSummary() { - List success = Collections.singletonList(new DeleteTokensSuccess(0, "tok-1")); - List errors = Collections.singletonList(new ErrorRecord(1, "failed", 404)); + public void testBulkDeleteTokensResponse_twoArgConstructorComputesSummary() { + List records = Arrays.asList( + new BulkDeleteTokensResponseRecord(0, "tok-1", 200, null), + new BulkDeleteTokensResponseRecord(1, "tok-2", 404, "Token not found")); List originalPayload = Arrays.asList("tok-1", "tok-2"); - BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(success, errors, originalPayload); + BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(records, originalPayload); Assert.assertNotNull(response.getSummary()); Assert.assertEquals(2, response.getSummary().getTotalTokens()); Assert.assertEquals(1, response.getSummary().getTotalDeleted()); Assert.assertEquals(1, response.getSummary().getTotalFailed()); + Assert.assertEquals(records, response.getRecords()); } @Test - public void testBulkDeleteTokensResponse_toStringNotNull() { - BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(Collections.emptyList(), Collections.emptyList()); - Assert.assertNotNull(response.toString()); + public void testBulkDeleteTokensResponse_getTokensToRetryFiltersRetryableStatusCodesOnly() { + List records = Arrays.asList( + new BulkDeleteTokensResponseRecord(0, "tok-0", 500, "server error"), // retryable (lower bound) + new BulkDeleteTokensResponseRecord(1, "tok-1", 404, "not found"), // not retryable + new BulkDeleteTokensResponseRecord(2, "tok-2", 599, "server error"), // retryable (upper bound) + new BulkDeleteTokensResponseRecord(3, "tok-3", 529, "special case"), // explicitly excluded + new BulkDeleteTokensResponseRecord(4, "tok-4", 200, null)); // succeeded + + BulkDeleteTokensResponse response = new BulkDeleteTokensResponse( + records, Arrays.asList("tok-0", "tok-1", "tok-2", "tok-3", "tok-4")); + + List tokensToRetry = response.getTokensToRetry(); + + Assert.assertEquals(2, tokensToRetry.size()); + Assert.assertTrue(tokensToRetry.contains("tok-0")); + Assert.assertTrue(tokensToRetry.contains("tok-2")); + Assert.assertFalse(tokensToRetry.contains("tok-1")); + Assert.assertFalse(tokensToRetry.contains("tok-3")); + Assert.assertFalse(tokensToRetry.contains("tok-4")); + } + + @Test + public void testBulkDeleteTokensResponse_getTokensToRetryEmptyWhenAllSucceeded() { + List records = Collections.singletonList( + new BulkDeleteTokensResponseRecord(0, "tok-0", 200, null)); + + BulkDeleteTokensResponse response = + new BulkDeleteTokensResponse(records, Collections.singletonList("tok-0")); + + Assert.assertTrue(response.getTokensToRetry().isEmpty()); + } + + @Test + public void testBulkDeleteTokensResponse_tokensToRetryNotSerialized() { + List records = Collections.singletonList( + new BulkDeleteTokensResponseRecord(0, "tok-0", 500, "server error")); + BulkDeleteTokensResponse response = + new BulkDeleteTokensResponse(records, Collections.singletonList("tok-0")); + + response.getTokensToRetry(); // populate the lazily-derived field + + Assert.assertFalse(response.toString().contains("tokensToRetry")); + } + + @Test + public void testBulkDeleteTokensResponse_toStringMatchesContractShape() { + List records = Arrays.asList( + new BulkDeleteTokensResponseRecord(0, "a1b2c3d4", 200, null), + new BulkDeleteTokensResponseRecord(1, "z9y8x7", 404, "Token not found")); + BulkDeleteTokensResponse response = + new BulkDeleteTokensResponse(records, Arrays.asList("a1b2c3d4", "z9y8x7")); + + String json = response.toString(); + + Assert.assertNotNull(json); + // summary + records only; originalPayload is internal and must never be serialized + Assert.assertTrue(json.contains("\"summary\"")); + Assert.assertTrue(json.contains("\"records\"")); + Assert.assertFalse(json.contains("originalPayload")); + // nulls are serialized so a success record still reports its error field + Assert.assertTrue(json.contains("\"error\":null")); + Assert.assertTrue(json.contains("\"index\":0")); + Assert.assertTrue(json.contains("\"httpCode\":200")); } // ── BulkTokenizeResponse ───────────────────────────────────────────────── + private static BulkTokenizeResponseRecord tokenizeRecord(int index, Object value, TokenizeResponseToken... tokens) { + return new BulkTokenizeResponseRecord(index, value, Arrays.asList(tokens)); + } + + private static TokenizeResponseToken okToken(String group, String token) { + return new TokenizeResponseToken(group, token, 200, null); + } + + private static TokenizeResponseToken failedToken(String group, String error) { + return new TokenizeResponseToken(group, null, 400, error); + } + + private static List payloadOf(int size) { + List payload = new ArrayList<>(); + for (int i = 0; i < size; i++) { + payload.add(BulkTokenizeRequestRecord.builder().value("v" + i).build()); + } + return payload; + } + @Test - public void testBulkTokenizeResponse_twoArgConstructorLeavesSummaryNull() { - List success = Collections.emptyList(); - List errors = Collections.emptyList(); + public void testBulkTokenizeResponse_singleArgConstructorLeavesSummaryNull() { + List records = Collections.emptyList(); - BulkTokenizeResponse response = new BulkTokenizeResponse(success, errors); + BulkTokenizeResponse response = new BulkTokenizeResponse(records); Assert.assertNull(response.getSummary()); - Assert.assertEquals(success, response.getSuccess()); - Assert.assertEquals(errors, response.getErrors()); + Assert.assertEquals(records, response.getRecords()); } @Test - public void testBulkTokenizeResponse_threeArgConstructorClassifiesEachIndex() { - // index 0: success only -> totalTokenized - // index 1: success AND error -> totalPartial - // index 2: error only -> totalFailed - List success = Arrays.asList( - new TokenizeSuccess(0, "v0"), - new TokenizeSuccess(1, "v1")); - List errors = Arrays.asList( - new ErrorRecord(1, "partial failure", 500), - new ErrorRecord(2, "full failure", 404)); - List originalPayload = Arrays.asList( - BulkTokenizeRecord.builder().value("v0").build(), - BulkTokenizeRecord.builder().value("v1").build(), - BulkTokenizeRecord.builder().value("v2").build()); + public void testBulkTokenizeResponse_twoArgConstructorClassifiesEachValue() { + // index 0: all groups ok -> totalTokenized + // index 1: some ok, some failed -> totalPartial + // index 2: all groups failed -> totalFailed + List records = Arrays.asList( + tokenizeRecord(0, "v0", okToken("g1", "tok-0")), + tokenizeRecord(1, "v1", okToken("g1", "tok-1"), failedToken("g2", "partial failure")), + tokenizeRecord(2, "v2", failedToken("g1", "full failure"))); - BulkTokenizeResponse response = new BulkTokenizeResponse(success, errors, originalPayload); + BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(3)); TokenizeSummary summary = response.getSummary(); Assert.assertNotNull(summary); + // totalTokens counts input values submitted, not output token entries Assert.assertEquals(3, summary.getTotalTokens()); Assert.assertEquals(1, summary.getTotalTokenized()); Assert.assertEquals(1, summary.getTotalPartial()); @@ -326,8 +399,140 @@ public void testBulkTokenizeResponse_threeArgConstructorClassifiesEachIndex() { } @Test - public void testBulkTokenizeResponse_toStringNotNull() { - BulkTokenizeResponse response = new BulkTokenizeResponse(Collections.emptyList(), Collections.emptyList()); - Assert.assertNotNull(response.toString()); + public void testBulkTokenizeResponse_recordWithNoTokensCountsAsFailed() { + List records = Collections.singletonList( + new BulkTokenizeResponseRecord(0, "v0", Collections.emptyList())); + + BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(1)); + + Assert.assertEquals(0, response.getSummary().getTotalTokenized()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetryReturnsCallerRecordUnchanged() { + // one value, four groups: only the 503 is retryable, but the whole record comes back + List records = Collections.singletonList( + tokenizeRecord(0, "9999999999", + okToken("phone_group", "p1q2r3s4"), + new TokenizeResponseToken("phone_group_2", null, 503, "unavailable"), + new TokenizeResponseToken("phone_group_3", null, 400, "bad group"), + new TokenizeResponseToken("phone_group_4", null, 529, "special case"))); + BulkTokenizeRequestRecord requested = BulkTokenizeRequestRecord.builder().value("9999999999") + .tokenGroupNames(Arrays.asList( + "phone_group", "phone_group_2", "phone_group_3", "phone_group_4")) + .build(); + + List retry = + new BulkTokenizeResponse(records, Collections.singletonList(requested)).getRecordsToRetry(); + + Assert.assertEquals(1, retry.size()); + // the caller's own object is handed straight back, groups and all + Assert.assertSame(requested, retry.get(0)); + Assert.assertEquals("9999999999", retry.get(0).getValue()); + Assert.assertEquals(Arrays.asList( + "phone_group", "phone_group_2", "phone_group_3", "phone_group_4"), + retry.get(0).getTokenGroupNames()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetryOmitsValuesWithoutRetryableFailures() { + List records = Arrays.asList( + tokenizeRecord(0, "v0", okToken("g1", "tok-0")), + tokenizeRecord(1, "v1", failedToken("g1", "bad group")), // 400, not retryable + tokenizeRecord(2, "v2", new TokenizeResponseToken("g1", null, 500, "server error"))); + + List retry = + new BulkTokenizeResponse(records, payloadOf(3)).getRecordsToRetry(); + + Assert.assertEquals(1, retry.size()); + Assert.assertEquals("v2", retry.get(0).getValue()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetryLooksUpByIndexNotResponseOrder() { + // batches finish out of order, so the failing record is not first in the response + List records = Arrays.asList( + tokenizeRecord(2, "v2", new TokenizeResponseToken("g1", null, 500, "server error")), + tokenizeRecord(0, "v0", okToken("g1", "tok-0"))); + + List retry = + new BulkTokenizeResponse(records, payloadOf(3)).getRecordsToRetry(); + + Assert.assertEquals(1, retry.size()); + Assert.assertEquals("v2", retry.get(0).getValue()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetrySkipsIndexOutsidePayload() { + // a malformed index must not blow up with IndexOutOfBoundsException + List records = Arrays.asList( + tokenizeRecord(9, "stray", new TokenizeResponseToken("g1", null, 500, "server error")), + tokenizeRecord(-1, "stray", new TokenizeResponseToken("g1", null, 500, "server error")), + tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + + List retry = + new BulkTokenizeResponse(records, payloadOf(1)).getRecordsToRetry(); + + Assert.assertEquals(1, retry.size()); + Assert.assertEquals("v0", retry.get(0).getValue()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetryCarriesByotToken() { + List records = Collections.singletonList( + tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + List payload = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("v0").token("my-own-token") + .tokenGroupNames(Collections.singletonList("g1")).build()); + + List retry = + new BulkTokenizeResponse(records, payload).getRecordsToRetry(); + + Assert.assertEquals("my-own-token", retry.get(0).getToken()); + } + + @Test + public void testBulkTokenizeResponse_getRecordsToRetryKeepsRequestedGroupsOnBatchFailure() { + // a batch-level failure reports no group name on the token entry + List records = Collections.singletonList( + tokenizeRecord(0, "v0", new TokenizeResponseToken(null, null, 500, "server error"))); + List payload = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("v0") + .tokenGroupNames(Arrays.asList("g1", "g2")).build()); + + List retry = + new BulkTokenizeResponse(records, payload).getRecordsToRetry(); + + Assert.assertEquals(Arrays.asList("g1", "g2"), retry.get(0).getTokenGroupNames()); + } + + @Test + public void testBulkTokenizeResponse_recordsToRetryNotSerialized() { + List records = Collections.singletonList( + tokenizeRecord(0, "v0", new TokenizeResponseToken("g1", null, 500, "server error"))); + BulkTokenizeResponse response = new BulkTokenizeResponse(records, payloadOf(1)); + + response.getRecordsToRetry(); // populate the lazily-derived field + + Assert.assertFalse(response.toString().contains("recordsToRetry")); + } + + @Test + public void testBulkTokenizeResponse_toStringMatchesContractShape() { + List records = Arrays.asList( + tokenizeRecord(0, "john@example.com", okToken("email_group", "a1b2c3d4")), + tokenizeRecord(1, "9999999999", + okToken("phone_group", "p1q2r3s4"), + failedToken("phone_group_2", "Invalid token group configuration"))); + + String json = new BulkTokenizeResponse(records, payloadOf(2)).toString(); + + Assert.assertTrue(json.contains("\"summary\"")); + Assert.assertTrue(json.contains("\"records\"")); + Assert.assertFalse(json.contains("originalPayload")); + Assert.assertTrue(json.contains("\"error\":null")); + Assert.assertTrue(json.contains("\"index\":0")); + Assert.assertTrue(json.contains("\"tokenGroupName\":\"email_group\"")); } } diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java new file mode 100644 index 00000000..d8a1437a --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkRetryAndSummaryTests.java @@ -0,0 +1,322 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Summary classification and retry-list derivation on the bulk responses. + * + *

Retryable is defined as "has an error AND a 5xx status other than 529", so each conjunct is + * exercised separately — a 4xx failure, a 5xx with no error, a null status, and the 529 carve-out + * must all stay out of the retry list. + */ +public class BulkRetryAndSummaryTests { + + private static TokenizeResponseToken token(String group, String value, Integer httpCode, String error) { + return new TokenizeResponseToken(group, value, httpCode, error); + } + + private static BulkTokenizeResponseRecord tokenizeRecord(int index, TokenizeResponseToken... tokens) { + return new BulkTokenizeResponseRecord(index, "value" + index, Arrays.asList(tokens)); + } + + private static BulkTokenizeRequestRecord requestRecord(String value) { + return (BulkTokenizeRequestRecord) BulkTokenizeRequestRecord.builder().value(value).build(); + } + + // ── BulkTokenizeResponse.buildSummary ───────────────────────────────────── + + @Test + public void testTokenizeSummary_allGroupsSucceededCountsAsTokenized() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", "t2", 200, null))); + + TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getSummary(); + + Assert.assertEquals(1, summary.getTotalTokens()); + Assert.assertEquals(1, summary.getTotalTokenized()); + Assert.assertEquals(0, summary.getTotalPartial()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_someGroupsFailedCountsAsPartial() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", null, 400, "bad group"))); + + TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getSummary(); + + Assert.assertEquals(0, summary.getTotalTokenized()); + Assert.assertEquals(1, summary.getTotalPartial()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_everyGroupFailedCountsAsFailed() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g1", null, 500, "boom"), token("g2", null, 500, "boom"))); + + TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getSummary(); + + Assert.assertEquals(0, summary.getTotalTokenized()); + Assert.assertEquals(0, summary.getTotalPartial()); + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_noTokensAtAllCountsAsFailed() { + // A record that came back with no token groups is a failure, not a success. + List records = Collections.singletonList( + new BulkTokenizeResponseRecord(0, "value0", null)); + + TokenizeSummary summary = new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getSummary(); + + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_emptyTokenListCountsAsFailed() { + List records = Collections.singletonList( + new BulkTokenizeResponseRecord(0, "value0", new ArrayList<>())); + + Assert.assertEquals(1, new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getSummary().getTotalFailed()); + } + + @Test + public void testTokenizeSummary_totalTokensComesFromTheSubmittedPayload() { + // Two values submitted, only one came back — totalTokens must reflect what was sent. + List records = Collections.singletonList( + tokenizeRecord(0, token("g1", "t1", 200, null))); + + TokenizeSummary summary = new BulkTokenizeResponse( + records, Arrays.asList(requestRecord("a"), requestRecord("b"))).getSummary(); + + Assert.assertEquals(2, summary.getTotalTokens()); + Assert.assertEquals(1, summary.getTotalTokenized()); + } + + @Test + public void testTokenizeSummary_nullRecordsYieldsZeroes() { + TokenizeSummary summary = new BulkTokenizeResponse(null, new ArrayList<>()).getSummary(); + + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalTokenized()); + Assert.assertEquals(0, summary.getTotalPartial()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_isNullWhenNoPayloadWasSupplied() { + // The single-arg constructor is the "no summary" path used before batching completes. + Assert.assertNull(new BulkTokenizeResponse(new ArrayList<>()).getSummary()); + } + + // ── BulkTokenizeResponse.getRecordsToRetry ──────────────────────────────── + + @Test + public void testTokenizeRetry_only5xxFailuresAreReturned() { + List records = Arrays.asList( + tokenizeRecord(0, token("g", null, 500, "server")), + tokenizeRecord(1, token("g", null, 400, "client")), + tokenizeRecord(2, token("g", "t", 200, null))); + List payload = + Arrays.asList(requestRecord("a"), requestRecord("b"), requestRecord("c")); + + List retry = new BulkTokenizeResponse(records, payload).getRecordsToRetry(); + + Assert.assertEquals(1, retry.size()); + Assert.assertSame("must return the caller's own record object", payload.get(0), retry.get(0)); + } + + @Test + public void testTokenizeRetry_529IsNotRetried() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g", null, 529, "site frozen"))); + + Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_599IsRetriedAnd600IsNot() { + Assert.assertEquals(1, new BulkTokenizeResponse( + Collections.singletonList(tokenizeRecord(0, token("g", null, 599, "edge"))), + Collections.singletonList(requestRecord("a"))).getRecordsToRetry().size()); + Assert.assertEquals(0, new BulkTokenizeResponse( + Collections.singletonList(tokenizeRecord(0, token("g", null, 600, "edge"))), + Collections.singletonList(requestRecord("a"))).getRecordsToRetry().size()); + } + + @Test + public void testTokenizeRetry_5xxWithoutAnErrorIsNotRetried() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g", "t", 500, null))); + + Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_nullHttpCodeIsNotRetried() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g", null, null, "no status"))); + + Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_partialFailureRetriesTheWholeRecord() { + // One group failed retryably, another succeeded — the value still needs resubmitting. + List records = Collections.singletonList( + tokenizeRecord(0, token("g1", "t1", 200, null), token("g2", null, 503, "down"))); + + Assert.assertEquals(1, new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().size()); + } + + @Test + public void testTokenizeRetry_recordWithNullTokensIsNotRetried() { + List records = Collections.singletonList( + new BulkTokenizeResponseRecord(0, "value0", null)); + + Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_outOfRangeIndexIsSkippedRatherThanThrowing() { + List records = Arrays.asList( + tokenizeRecord(5, token("g", null, 500, "server")), + tokenizeRecord(-1, token("g", null, 500, "server"))); + + Assert.assertTrue(new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))) + .getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_withoutOriginalPayloadReturnsEmpty() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g", null, 500, "server"))); + + Assert.assertTrue(new BulkTokenizeResponse(records).getRecordsToRetry().isEmpty()); + } + + @Test + public void testTokenizeRetry_isMemoisedAcrossCalls() { + List records = Collections.singletonList( + tokenizeRecord(0, token("g", null, 500, "server"))); + BulkTokenizeResponse response = + new BulkTokenizeResponse(records, Collections.singletonList(requestRecord("a"))); + + Assert.assertSame(response.getRecordsToRetry(), response.getRecordsToRetry()); + } + + // ── BulkDeleteTokensResponse ────────────────────────────────────────────── + + private static BulkDeleteTokensResponseRecord deleteRecord(int index, String token, Integer code, String error) { + return new BulkDeleteTokensResponseRecord(index, token, code, error); + } + + @Test + public void testDeleteTokensSummary_countsDeletedAndFailed() { + List records = Arrays.asList( + deleteRecord(0, "t1", 200, null), + deleteRecord(1, "t2", 404, "not found")); + + DeleteTokensSummary summary = + new BulkDeleteTokensResponse(records, Arrays.asList("t1", "t2")).getSummary(); + + Assert.assertEquals(2, summary.getTotalTokens()); + Assert.assertEquals(1, summary.getTotalDeleted()); + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + public void testDeleteTokensSummary_totalFallsBackToRecordCountWithoutPayload() { + List records = Arrays.asList( + deleteRecord(0, "t1", 200, null), + deleteRecord(1, "t2", 500, "boom")); + + DeleteTokensSummary summary = new BulkDeleteTokensResponse(records, null).getSummary(); + + Assert.assertEquals(2, summary.getTotalTokens()); + } + + @Test + public void testDeleteTokensSummary_nullRecordsYieldsZeroes() { + DeleteTokensSummary summary = new BulkDeleteTokensResponse(null, new ArrayList<>()).getSummary(); + + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalDeleted()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testDeleteTokensRetry_only5xxFailuresAreReturned() { + List records = Arrays.asList( + deleteRecord(0, "t1", 500, "server"), + deleteRecord(1, "t2", 400, "client"), + deleteRecord(2, "t3", 200, null)); + + List retry = new BulkDeleteTokensResponse(records, Arrays.asList("t1", "t2", "t3")) + .getTokensToRetry(); + + Assert.assertEquals(Collections.singletonList("t1"), retry); + } + + @Test + public void testDeleteTokensRetry_529IsNotRetried() { + List records = + Collections.singletonList(deleteRecord(0, "t1", 529, "site frozen")); + + Assert.assertTrue(new BulkDeleteTokensResponse(records, Collections.singletonList("t1")) + .getTokensToRetry().isEmpty()); + } + + @Test + public void testDeleteTokensRetry_5xxWithoutErrorOrNullCodeIsNotRetried() { + Assert.assertTrue(new BulkDeleteTokensResponse( + Collections.singletonList(deleteRecord(0, "t1", 500, null)), + Collections.singletonList("t1")).getTokensToRetry().isEmpty()); + Assert.assertTrue(new BulkDeleteTokensResponse( + Collections.singletonList(deleteRecord(0, "t1", null, "no status")), + Collections.singletonList("t1")).getTokensToRetry().isEmpty()); + } + + @Test + public void testDeleteTokensRetry_nullTokenIsSkipped() { + // A failed record with no token echoed back cannot be resubmitted. + List records = + Collections.singletonList(deleteRecord(0, null, 500, "server")); + + Assert.assertTrue(new BulkDeleteTokensResponse(records, Collections.singletonList("t1")) + .getTokensToRetry().isEmpty()); + } + + @Test + public void testDeleteTokensRetry_isMemoisedAcrossCalls() { + List records = + Collections.singletonList(deleteRecord(0, "t1", 500, "server")); + BulkDeleteTokensResponse response = + new BulkDeleteTokensResponse(records, Collections.singletonList("t1")); + + Assert.assertSame(response.getTokensToRetry(), response.getTokensToRetry()); + } + + @Test + public void testDeleteTokensRetry_withNullRecordsReturnsEmpty() { + Assert.assertTrue(new BulkDeleteTokensResponse(null, Collections.singletonList("t1")) + .getTokensToRetry().isEmpty()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java index f88c7a39..83b54775 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java @@ -2,12 +2,6 @@ import org.junit.Assert; import org.junit.Test; - -/** - * Tests for the single-field options/builder classes: {@link InsertOptions}, - * {@link DetokenizeOptions}, {@link TokenizeOptions} and {@link DeleteTokensOptions}. - * Each class simply wraps a {@link RequestInterceptor} with no validation. - */ public class OptionsTests { private static final RequestInterceptor INTERCEPTOR = context -> { @@ -41,6 +35,37 @@ public void testDetokenizeOptions_withoutInterceptor() { Assert.assertNull(options.getInterceptor()); } + // ── BulkInsertOptions ──────────────────────────────────────────────────── + + @Test + public void testBulkInsertOptions_withInterceptor() { + BulkInsertOptions options = BulkInsertOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + // BulkInsertOptions is a specialization of InsertOptions, so it flows anywhere the base does + Assert.assertTrue(options instanceof InsertOptions); + } + + @Test + public void testBulkInsertOptions_withoutInterceptor() { + BulkInsertOptions options = BulkInsertOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + // ── BulkDetokenizeOptions ──────────────────────────────────────────────── + + @Test + public void testBulkDetokenizeOptions_withInterceptor() { + BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + Assert.assertTrue(options instanceof DetokenizeOptions); + } + + @Test + public void testBulkDetokenizeOptions_withoutInterceptor() { + BulkDetokenizeOptions options = BulkDetokenizeOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + // ── TokenizeOptions ────────────────────────────────────────────────────── @Test @@ -68,4 +93,67 @@ public void testDeleteTokensOptions_withoutInterceptor() { DeleteTokensOptions options = DeleteTokensOptions.builder().build(); Assert.assertNull(options.getInterceptor()); } + + // ── BulkTokenizeOptions ────────────────────────────────────────────────── + + @Test + public void testBulkTokenizeOptions_withInterceptor() { + BulkTokenizeOptions options = BulkTokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + public void testBulkTokenizeOptions_withoutInterceptor() { + BulkTokenizeOptions options = BulkTokenizeOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + @Test + public void testBulkTokenizeOptions_isATokenizeOptions() { + BulkTokenizeOptions options = BulkTokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertTrue(options instanceof TokenizeOptions); + // the inherited accessor sees the same interceptor + Assert.assertSame(INTERCEPTOR, ((TokenizeOptions) options).getInterceptor()); + } + + @Test + public void testBulkTokenizeOptions_builderStaysBulkTypedWhileChaining() { + // builder() hides the parent's, so the override must return the bulk builder for + // chaining to keep compiling without a cast + BulkTokenizeOptions.BulkTokenizeOptionsBuilder builder = + BulkTokenizeOptions.builder().interceptor(INTERCEPTOR); + BulkTokenizeOptions options = builder.build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + // ── BulkDeleteTokensOptions ────────────────────────────────────────────── + + @Test + public void testBulkDeleteTokensOptions_withInterceptor() { + BulkDeleteTokensOptions options = + BulkDeleteTokensOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + public void testBulkDeleteTokensOptions_withoutInterceptor() { + BulkDeleteTokensOptions options = BulkDeleteTokensOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + @Test + public void testBulkDeleteTokensOptions_isADeleteTokensOptions() { + BulkDeleteTokensOptions options = + BulkDeleteTokensOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertTrue(options instanceof DeleteTokensOptions); + Assert.assertSame(INTERCEPTOR, ((DeleteTokensOptions) options).getInterceptor()); + } + + @Test + public void testBulkDeleteTokensOptions_builderStaysBulkTypedWhileChaining() { + BulkDeleteTokensOptions.BulkDeleteTokensOptionsBuilder builder = + BulkDeleteTokensOptions.builder().interceptor(INTERCEPTOR); + BulkDeleteTokensOptions options = builder.build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } } diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java index 5e6ee8f8..52cbf10c 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java @@ -11,7 +11,7 @@ /** * Tests for the plain record/redaction data holders: {@link TokenGroupRedactions}, * {@link InsertRequestRecord}, {@link BulkInsertRequestRecord} - * and {@link BulkTokenizeRecord}. None of these classes perform + * and {@link BulkTokenizeRequestRecord}. None of these classes perform * validation in their builders, so coverage here is builder-construction plus getters. */ public class RecordAndRedactionTests { @@ -111,33 +111,55 @@ public void testBulkInsertRequestRecord_defaultsAreNull() { Assert.assertNull(record.getUpsert()); } + // ── TokenizeRequestRecord ─────────────────────────────────────────────────────── + @Test - public void testBulkInsertRequestRecord_isAnInsertRequestRecord() { - InsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("persons").build(); - Assert.assertTrue(record instanceof BulkInsertRequestRecord); - Assert.assertEquals("persons", record.getTableName()); + public void testTokenizeRecord_gettersReturnBuilderValues() { + List groups = Arrays.asList("group1", "group2"); + TokenizeRequestRecord record = TokenizeRequestRecord.builder() + .value("secret-value") + .tokenGroupNames(groups) + .build(); + + Assert.assertEquals("secret-value", record.getValue()); + Assert.assertEquals(groups, record.getTokenGroupNames()); } - // Tests for TokenizeRecord were removed: the class no longer exists (bulk-only module). + @Test + public void testTokenizeRecord_defaultsAreNull() { + TokenizeRequestRecord record = TokenizeRequestRecord.builder().build(); + Assert.assertNull(record.getValue()); + Assert.assertNull(record.getTokenGroupNames()); + } - // ── BulkTokenizeRecord ─────────────────────────────────────────────────── + // ── BulkTokenizeRequestRecord ─────────────────────────────────────────────────── @Test - public void testBulkTokenizeRecord_gettersReturnBuilderValues() { + public void testBulkTokenizeRequestRecord_gettersReturnBuilderValues() { List groups = Arrays.asList("group3"); - BulkTokenizeRecord record = BulkTokenizeRecord.builder() + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder() .value(12345) + .token("byot-token") .tokenGroupNames(groups) .build(); Assert.assertEquals(12345, record.getValue()); + Assert.assertEquals("byot-token", record.getToken()); Assert.assertEquals(groups, record.getTokenGroupNames()); } @Test - public void testBulkTokenizeRecord_defaultsAreNull() { - BulkTokenizeRecord record = BulkTokenizeRecord.builder().build(); + public void testBulkTokenizeRequestRecord_defaultsAreNull() { + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder().build(); Assert.assertNull(record.getValue()); + Assert.assertNull(record.getToken()); Assert.assertNull(record.getTokenGroupNames()); } + + @Test + public void testBulkTokenizeRequestRecord_isATokenizeRequestRecord() { + // the bulk record adds nothing today; it must still satisfy the parent contract + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder().value("v1").build(); + Assert.assertTrue(record instanceof TokenizeRequestRecord); + } } 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 9e00b238..393a98d1 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java @@ -80,52 +80,54 @@ public void testInsertRequestRecord_gettersReturnBuilderValues() { // ── UpsertOptions ──────────────────────────────────────────────────────── @Test - public void testUpsertOptions_gettersReturnBuilderValues() { - List uniqueColumns = Arrays.asList("id", "email"); - UpsertOptions upsert = UpsertOptions.builder() - .updateType("REPLACE") - .uniqueColumns(uniqueColumns) - .build(); + public void testTokenizeRequest_getterReturnsBuilderValue() { + List records = Collections.singletonList( + TokenizeRequestRecord.builder().value("v1").build()); + TokenizeRequest request = TokenizeRequest.builder().records(records).build(); + Assert.assertEquals(records, request.getRecords()); + } - Assert.assertEquals("REPLACE", upsert.getUpdateType()); - Assert.assertEquals(uniqueColumns, upsert.getUniqueColumns()); + @Test + public void testBulkTokenizeRequest_isATokenizeRequest() { + BulkTokenizeRequest request = BulkTokenizeRequest.builder() + .records(Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("v1").build())) + .build(); + Assert.assertTrue(request instanceof TokenizeRequest); + // the inherited accessor sees the same records, widened + Assert.assertEquals(1, ((TokenizeRequest) request).getRecords().size()); } - // Tests for InsertResponse were removed: the class no longer exists (bulk-only module). + @Test + public void testTokenizeRequest_defaultIsNull() { + TokenizeRequest request = TokenizeRequest.builder().build(); + Assert.assertNull(request.getRecords()); + } - // ── InsertResponseRecord ───────────────────────────────────────────────── + // ── TokenizeResponse ───────────────────────────────────────────────────── @Test - public void testInsertResponseRecord_gettersReturnConstructorValues() { - Map fields = new HashMap<>(); - fields.put("name", "tok-abc"); - Map hashedData = new HashMap<>(); - hashedData.put("name", "hashed-value"); + public void testTokenizeResponse_gettersReturnConstructorValues() { + List records = Collections.singletonList( + new TokenizeResponseRecord("value1", Collections.singletonList( + new TokenizeResponseToken("group1", "tok-abc", 200, null)))); - InsertResponseRecord record = new InsertResponseRecord( - "persons", "id-1", fields, hashedData, 200, null); + TokenizeResponse response = new TokenizeResponse(records); - Assert.assertEquals("persons", record.getTableName()); - Assert.assertEquals("id-1", record.getSkyflowId()); - Assert.assertEquals(fields, record.getFields()); - Assert.assertEquals(hashedData, record.getHashedData()); - Assert.assertEquals(200, record.getHttpCode()); - Assert.assertNull(record.getError()); + Assert.assertEquals(records, response.getResponse()); + Assert.assertEquals("value1", response.getResponse().get(0).getValue()); + Assert.assertEquals("tok-abc", response.getResponse().get(0).getTokens().get(0).getToken()); + Assert.assertNull(response.getResponse().get(0).getTokens().get(0).getError()); } @Test - public void testInsertResponseRecord_errorCase() { - InsertResponseRecord record = new InsertResponseRecord( - "persons", null, null, null, 400, "insert failed"); - - Assert.assertEquals("persons", record.getTableName()); - Assert.assertNull(record.getSkyflowId()); - Assert.assertEquals("insert failed", record.getError()); - Assert.assertEquals(400, record.getHttpCode()); + public void testTokenizeResponse_toStringSerializesNulls() { + TokenizeResponse response = new TokenizeResponse(Collections.singletonList( + new TokenizeResponseRecord("value1", Collections.singletonList( + new TokenizeResponseToken("group1", "tok-abc", 200, null))))); + Assert.assertTrue(response.toString().contains("\"error\":null")); } - // Tests for TokenizeRequest / TokenizeResponse were removed: those classes no longer exist (bulk-only module). - // ── DetokenizeRequest ──────────────────────────────────────────────────── @Test @@ -234,16 +236,16 @@ public void testBulkDetokenizeRequest_isADetokenizeRequest() { @Test public void testBulkTokenizeRequest_getterReturnsBuilderValue() { - ArrayList data = new ArrayList<>(Collections.singletonList( - BulkTokenizeRecord.builder().value("v1").build())); - BulkTokenizeRequest request = BulkTokenizeRequest.builder().data(data).build(); - Assert.assertEquals(data, request.getData()); + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder().value("v1").build()); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); + Assert.assertEquals(records, request.getRecords()); } @Test public void testBulkTokenizeRequest_defaultIsNull() { BulkTokenizeRequest request = BulkTokenizeRequest.builder().build(); - Assert.assertNull(request.getData()); + Assert.assertNull(request.getRecords()); } // ── BulkInsertRequest ──────────────────────────────────────────────────── @@ -279,10 +281,18 @@ public void testBulkInsertRequest_defaultsAreNull() { } @Test - public void testBulkInsertRequest_isAnInsertRequest() { - InsertRequest request = BulkInsertRequest.builder().tableName("persons").build(); - Assert.assertTrue(request instanceof BulkInsertRequest); - Assert.assertEquals("persons", request.getTableName()); + public void testDeleteTokensResponse_gettersReturnConstructorValues() { + List records = Arrays.asList( + new DeleteTokensRecord("tok-1", 200, null), + new DeleteTokensRecord("tok-2", 404, "Token not found")); + + DeleteTokensResponse response = new DeleteTokensResponse(records); + + Assert.assertEquals(records, response.getRecords()); + Assert.assertEquals("tok-1", response.getRecords().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getError()); + Assert.assertEquals("Token not found", response.getRecords().get(1).getError()); + Assert.assertEquals(Integer.valueOf(404), response.getRecords().get(1).getHttpCode()); } // Tests for DeleteTokensResponse were removed: the class no longer exists (bulk-only module). 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 f8e30a93..273c4c85 100644 --- a/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -3,16 +3,19 @@ import org.junit.Assert; import org.junit.Test; +import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** * Tests for the response/success/summary building-block classes that carry real - * constructor logic or toString() serialization: {@link BulkInsertResponseRecord}, - * {@link BulkSummary}, {@link TokenizeSuccess}, {@link TokenizeSummary}, - * {@link DeleteTokensSuccess}, {@link DeleteTokensSummary}, {@link DetokenizeSummary}, - * {@link ErrorRecord} and {@link BulkDetokenizeResponseRecord}. + * constructor logic or toString() serialization: {@link Success}, {@link Summary}, + * {@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 { @@ -88,29 +91,51 @@ public void testBulkSummary_toStringNotNull() { Assert.assertNotNull(new BulkSummary(1, 1, 0).toString()); } - // Tests for TokenizeData were removed: the class no longer exists (bulk-only module). - // ── TokenizeSuccess ────────────────────────────────────────────────────── + // ── TokenizeResponseToken ──────────────────────────────────────────────── @Test - public void testTokenizeSuccess_getIndexAndValueAndEmptyTokensOnConstruction() { - TokenizeSuccess success = new TokenizeSuccess(3, "value-1"); - Assert.assertEquals(3, success.getIndex()); - Assert.assertEquals("value-1", success.getValue()); - Assert.assertNotNull(success.getTokens()); - Assert.assertTrue(success.getTokens().isEmpty()); + public void testTokenizeResponseToken_successValues() { + TokenizeResponseToken token = new TokenizeResponseToken("group1", "tok-abc", 200, null); + Assert.assertEquals("group1", token.getTokenGroupName()); + Assert.assertEquals("tok-abc", token.getToken()); + Assert.assertEquals(Integer.valueOf(200), token.getHttpCode()); + Assert.assertNull(token.getError()); } @Test - public void testTokenizeSuccess_addTokenPopulatesTokensMap() { - TokenizeSuccess success = new TokenizeSuccess(0, "value-1"); - success.addToken("group1", "tok-1"); - Assert.assertEquals("tok-1", success.getTokens().get("group1")); + public void testTokenizeResponseToken_errorValues() { + TokenizeResponseToken token = new TokenizeResponseToken("group2", null, 400, "bad group"); + Assert.assertNull(token.getToken()); + Assert.assertEquals("bad group", token.getError()); + Assert.assertEquals(Integer.valueOf(400), token.getHttpCode()); } @Test - public void testTokenizeSuccess_toStringNotNull() { - Assert.assertNotNull(new TokenizeSuccess(0, "value").toString()); + public void testTokenizeResponseToken_toStringSerializesNulls() { + Assert.assertTrue(new TokenizeResponseToken("group1", "tok-abc", 200, null) + .toString().contains("\"error\":null")); + } + + // ── TokenizeResponseRecord / BulkTokenizeResponseRecord ────────────────── + + @Test + public void testTokenizeResponseRecord_gettersReturnConstructorValues() { + List tokens = Collections.singletonList( + new TokenizeResponseToken("group1", "tok-abc", 200, null)); + TokenizeResponseRecord record = new TokenizeResponseRecord("value1", tokens); + Assert.assertEquals("value1", record.getValue()); + Assert.assertEquals(tokens, record.getTokens()); + } + + @Test + public void testBulkTokenizeResponseRecord_carriesIndexAndIsATokenizeResponseRecord() { + BulkTokenizeResponseRecord record = new BulkTokenizeResponseRecord(7, "value1", + Collections.singletonList(new TokenizeResponseToken("group1", "tok-abc", 200, null))); + Assert.assertEquals(7, record.getIndex()); + Assert.assertEquals("value1", record.getValue()); + Assert.assertTrue(record instanceof TokenizeResponseRecord); + Assert.assertNotNull(record.toString()); } // ── TokenizeSummary ────────────────────────────────────────────────────── @@ -138,18 +163,48 @@ public void testTokenizeSummary_toStringNotNull() { Assert.assertNotNull(new TokenizeSummary(1, 1, 0, 0).toString()); } - // ── DeleteTokensSuccess ────────────────────────────────────────────────── + // ── DeleteTokensRecord ─────────────────────────────────────────────────── + + @Test + public void testDeleteTokensRecord_gettersReturnConstructorValues() { + DeleteTokensRecord record = new DeleteTokensRecord("tok-1", 200, null); + Assert.assertEquals("tok-1", record.getToken()); + Assert.assertEquals(Integer.valueOf(200), record.getHttpCode()); + Assert.assertNull(record.getError()); + } + + @Test + public void testDeleteTokensRecord_carriesErrorDetails() { + DeleteTokensRecord record = new DeleteTokensRecord("tok-2", 404, "Token not found"); + Assert.assertEquals("tok-2", record.getToken()); + Assert.assertEquals(Integer.valueOf(404), record.getHttpCode()); + Assert.assertEquals("Token not found", record.getError()); + } + + @Test + public void testDeleteTokensRecord_toStringSerializesNulls() { + Assert.assertTrue(new DeleteTokensRecord("tok-1", 200, null).toString().contains("\"error\":null")); + } + + // ── BulkDeleteTokensResponseRecord ─────────────────────────────────────── + + @Test + public void testBulkDeleteTokensResponseRecord_gettersReturnConstructorValues() { + BulkDeleteTokensResponseRecord record = new BulkDeleteTokensResponseRecord(1, "tok-1", 200, null); + Assert.assertEquals(1, record.getIndex()); + Assert.assertEquals("tok-1", record.getToken()); + Assert.assertEquals(Integer.valueOf(200), record.getHttpCode()); + Assert.assertNull(record.getError()); + } @Test - public void testDeleteTokensSuccess_gettersReturnConstructorValues() { - DeleteTokensSuccess success = new DeleteTokensSuccess(1, "tok-1"); - Assert.assertEquals(1, success.getIndex()); - Assert.assertEquals("tok-1", success.getToken()); + public void testBulkDeleteTokensResponseRecord_isADeleteTokensRecord() { + Assert.assertTrue(new BulkDeleteTokensResponseRecord(0, "tok-1", 200, null) instanceof DeleteTokensRecord); } @Test - public void testDeleteTokensSuccess_toStringNotNull() { - Assert.assertNotNull(new DeleteTokensSuccess(0, "tok-1").toString()); + public void testBulkDeleteTokensResponseRecord_toStringNotNull() { + Assert.assertNotNull(new BulkDeleteTokensResponseRecord(0, "tok-1", 200, null).toString()); } // ── DeleteTokensSummary ────────────────────────────────────────────────── diff --git a/pom.xml b/pom.xml index 9dc3d230..50d2382e 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ common - v2 + skyvault flowvault @@ -145,6 +145,43 @@ + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + copy-root-dotenv-for-tests + process-test-resources + + copy-resources + + + ${project.basedir} + true + + + ${maven.multiModuleProjectDirectory} + + .env + + false + + + + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/scripts/contract-snapshot-update.sh b/scripts/contract-snapshot-update.sh index 95201b9a..bd33397b 100755 --- a/scripts/contract-snapshot-update.sh +++ b/scripts/contract-snapshot-update.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Regenerates the v2 contract-testing baseline (api-report/skyflow-java.baseline.jar) +# Regenerates the skyvault contract-testing baseline (api-report/skyflow-java.baseline.jar) # from the CURRENT working tree and overwrites the committed snapshot. # # Run this after an intentional public API change to v2, review the resulting @@ -12,16 +12,16 @@ cd "$(dirname "$0")/.." mvn -B package -pl common,v2 -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true -SHADED_JAR=$(ls v2/target/skyflow-java-*-with-common.jar | head -n1) +SHADED_JAR=$(ls skyvault/target/skyflow-java-*-with-common.jar | head -n1) if [ -z "$SHADED_JAR" ]; then - echo "Error: could not find a built v2/target/skyflow-java-*-with-common.jar. Did the build succeed?" + echo "Error: could not find a built skyvault/target/skyflow-java-*-with-common.jar. Did the build succeed?" exit 1 fi -mkdir -p v2/api-report -cp "$SHADED_JAR" v2/api-report/skyflow-java.baseline.jar +mkdir -p skyvault/api-report +cp "$SHADED_JAR" skyvault/api-report/skyflow-java.baseline.jar echo "--------------------------" -echo "Updated v2/api-report/skyflow-java.baseline.jar from $SHADED_JAR" +echo "Updated skyvault/api-report/skyflow-java.baseline.jar from $SHADED_JAR" echo "Review the diff and commit this file alongside your API change." diff --git a/skyvault/api-report/skyflow-java.baseline.jar b/skyvault/api-report/skyflow-java.baseline.jar new file mode 100644 index 00000000..6911c011 Binary files /dev/null and b/skyvault/api-report/skyflow-java.baseline.jar differ diff --git a/v2/pom.xml b/skyvault/pom.xml similarity index 100% rename from v2/pom.xml rename to skyvault/pom.xml diff --git a/skyvault/samples/README.md b/skyvault/samples/README.md new file mode 100644 index 00000000..666b2186 --- /dev/null +++ b/skyvault/samples/README.md @@ -0,0 +1,84 @@ +# Java SDK samples +Test the SDK by adding `VAULT-ID`, `VAULT-URL`, and `SERVICE-ACCOUNT` details in the required places for each sample. + +## 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 +1. In a browser, sign in to Skyflow Studio. +2. Create a vault by clicking **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. + +### 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. + +## 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 diff --git a/samples/pom.xml b/skyvault/samples/pom.xml similarity index 100% rename from samples/pom.xml rename to skyvault/samples/pom.xml diff --git a/samples/src/main/java/com/example/connection/InvokeConnectionExample.java b/skyvault/samples/src/main/java/com/example/connection/InvokeConnectionExample.java similarity index 100% rename from samples/src/main/java/com/example/connection/InvokeConnectionExample.java rename to skyvault/samples/src/main/java/com/example/connection/InvokeConnectionExample.java diff --git a/samples/src/main/java/com/example/detect/DeidentifyFileExample.java b/skyvault/samples/src/main/java/com/example/detect/DeidentifyFileExample.java similarity index 100% rename from samples/src/main/java/com/example/detect/DeidentifyFileExample.java rename to skyvault/samples/src/main/java/com/example/detect/DeidentifyFileExample.java diff --git a/samples/src/main/java/com/example/detect/DeidentifyFileExampleAsync.java b/skyvault/samples/src/main/java/com/example/detect/DeidentifyFileExampleAsync.java similarity index 100% rename from samples/src/main/java/com/example/detect/DeidentifyFileExampleAsync.java rename to skyvault/samples/src/main/java/com/example/detect/DeidentifyFileExampleAsync.java diff --git a/samples/src/main/java/com/example/detect/DeidentifyTextExample.java b/skyvault/samples/src/main/java/com/example/detect/DeidentifyTextExample.java similarity index 100% rename from samples/src/main/java/com/example/detect/DeidentifyTextExample.java rename to skyvault/samples/src/main/java/com/example/detect/DeidentifyTextExample.java diff --git a/samples/src/main/java/com/example/detect/GetDetectRunExample.java b/skyvault/samples/src/main/java/com/example/detect/GetDetectRunExample.java similarity index 100% rename from samples/src/main/java/com/example/detect/GetDetectRunExample.java rename to skyvault/samples/src/main/java/com/example/detect/GetDetectRunExample.java diff --git a/samples/src/main/java/com/example/detect/ReidentifyTextExample.java b/skyvault/samples/src/main/java/com/example/detect/ReidentifyTextExample.java similarity index 100% rename from samples/src/main/java/com/example/detect/ReidentifyTextExample.java rename to skyvault/samples/src/main/java/com/example/detect/ReidentifyTextExample.java diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java new file mode 100644 index 00000000..48889218 --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java @@ -0,0 +1,88 @@ +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.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeData; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; +import io.github.cdimascio.dotenv.Dotenv; +import java.util.ArrayList; + +/** + * 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 detokenization request. The code also implements a retry + * mechanism to handle unauthorized access errors (HTTP 401). + */ +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 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 + DetokenizeData detokenizeDataToken1 = new DetokenizeData("", RedactionType.PLAIN_TEXT); + DetokenizeData detokenizeDataToken2 = new DetokenizeData(""); + ArrayList detokenizeDataList = new ArrayList<>(); + detokenizeDataList.add(detokenizeDataToken1); // First token + detokenizeDataList.add(detokenizeDataToken2); // Second token + + // Building a detokenization request with the token list and configuration + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeDataList) // Adding tokens to the request + .continueOnError(false) // Stop on error + .build(); + + // Sending the detokenization request and receiving the response + DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); + + // Printing the detokenized response + System.out.println(detokenizeResponse); + } +} diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java new file mode 100644 index 00000000..c4578545 --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java @@ -0,0 +1,66 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; +import com.skyflow.serviceaccount.util.Token; + +import java.io.File; + +/** + * Example program to generate a Bearer Token using Skyflow's BearerToken utility. + * The token can be generated in two ways: + * 1. Using the file path to a credentials.json file. + * 2. Using the JSON content of the credentials file as a string. + */ +public class BearerTokenGenerationExample { + public static void main(String[] args) { + // Variable to store the generated token + String token = null; + + // Example 1: Generate Bearer Token using a credentials.json file + try { + // Specify the full file path to the credentials.json file + String filePath = ""; + + // Check if the token is either not initialized or has expired + if (Token.isExpired(token)) { + // Create a BearerToken object using the credentials file + BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Set credentials from the file path + .build(); + + // Generate a new Bearer Token + token = bearerToken.getBearerToken(); + } + + // Print the generated Bearer Token to the console + System.out.println("Generated Bearer Token (from file): " + token); + } catch (SkyflowException e) { + // Handle any exceptions encountered during the token generation process + e.printStackTrace(); + } + + // Example 2: Generate Bearer Token using the credentials JSON as a string + try { + // Provide the credentials JSON content as a string + String fileContents = ""; + + // Check if the token is either not initialized or has expired + if (Token.isExpired(token)) { + // Create a BearerToken object using the credentials string + BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Set credentials from the string + .build(); + + // Generate a new Bearer Token + token = bearerToken.getBearerToken(); + } + + // Print the generated Bearer Token to the console + System.out.println("Generated Bearer Token (from string): " + token); + } catch (SkyflowException e) { + // Handle any exceptions encountered during the token generation process + e.printStackTrace(); + } + } +} diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java new file mode 100644 index 00000000..30d8f9ba --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java @@ -0,0 +1,72 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; + +/** + * This example demonstrates how to generate Bearer tokens in two different ways: + * 1. Using a credentials file specified by its file path. + * 2. Using the credentials as a string. + *

+ * The code also showcases multithreaded token generation with a shared context (`ctx`), + * where each thread generates and prints tokens repeatedly. + */ +public class BearerTokenGenerationUsingThreadsExample { + public static void main(String[] args) { + // Example 1: Generate Bearer token using a credentials file path + try { + // Step 1: Specify the path to the credentials file + String filePath = ""; // Replace with the actual file path + + // Step 2: Create a BearerToken object using the file path + final BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Provide the credentials file + .setCtx("abc") // Specify a context string ("abc" in this case) + .build(); + + // Step 3: Create and start a thread to repeatedly generate and print tokens + Thread t = new Thread(() -> { + for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times + try { + System.out.println(bearerToken.getBearerToken()); // Print the Bearer token + } catch (SkyflowException e) { // Handle exceptions during token generation + Thread.currentThread().interrupt(); // Interrupt the thread on error + throw new RuntimeException(e); // Wrap and propagate the exception + } + } + }); + t.start(); // Start the thread + } catch (Exception e) { // Handle exceptions during BearerToken creation + e.printStackTrace(); + } + + // Example 2: Generate Bearer token using credentials as a string + try { + // Step 1: Specify the credentials as a string (file contents) + String fileContents = ""; // Replace with actual file contents + + // Step 2: Create a BearerToken object using the credentials string + final BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Provide the credentials as a string + .setCtx("abc") // Specify a context string ("abc" in this case) + .build(); + + // Step 3: Create and start a thread to repeatedly generate and print tokens + Thread t = new Thread(() -> { + for (int i = 0; i < 5; i++) { // Loop to generate tokens 5 times + try { + System.out.println(bearerToken.getBearerToken()); // Print the Bearer token + } catch (SkyflowException e) { // Handle exceptions during token generation + Thread.currentThread().interrupt(); // Interrupt the thread on error + throw new RuntimeException(e); // Wrap and propagate the exception + } + } + }); + t.start(); // Start the thread + } catch (Exception e) { // Handle exceptions during BearerToken creation + e.printStackTrace(); + } + } +} diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java new file mode 100644 index 00000000..fcb2a407 --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java @@ -0,0 +1,73 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * Example program to generate a Bearer Token using Skyflow's BearerToken utility. + * The token is generated using three approaches: + * 1. By providing a string context. + * 2. By providing a JSON object context (Map) for conditional data access policies. + * 3. By providing the credentials as a string with context. + */ +public class BearerTokenGenerationWithContextExample { + public static void main(String[] args) { + String bearerToken = null; + + // Approach 1: Bearer token with string context + // Use a simple string identifier when your policy references a single context value. + try { + String filePath = ""; + BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx("user_12345") + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (string context): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Approach 2: Bearer token with JSON object context + // Use a structured Map when your policy needs multiple context values. + // Each key maps to a Skyflow CEL policy variable under request.context.* + // For example, the map below enables policies like: + // request.context.role == "admin" && request.context.department == "finance" + try { + String filePath = ""; + Map ctx = new HashMap<>(); + ctx.put("role", "admin"); + ctx.put("department", "finance"); + ctx.put("user_id", "user_12345"); + + BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx(ctx) + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (object context): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Approach 3: Bearer token with string context from credentials string + try { + String fileContents = ""; + BearerToken token = BearerToken.builder() + .setCredentials(fileContents) + .setCtx("user_12345") + .build(); + + bearerToken = token.getBearerToken(); + System.out.println("Bearer token (creds string): " + bearerToken); + } catch (SkyflowException e) { + e.printStackTrace(); + } + } +} diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java new file mode 100644 index 00000000..3cba8bd5 --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java @@ -0,0 +1,66 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; +import java.util.ArrayList; + +/** + * This example demonstrates how to generate a Scoped Bearer Token in two ways: + * 1. Using a credentials file specified by its file path. + * 2. Using the credentials as a string. + *

+ * Scoped tokens are generated by assigning specific roles for access control. + */ +public class ScopedTokenGenerationExample { + public static void main(String[] args) { + String scopedToken = null; // Variable to store the generated Scoped Bearer Token + + // Example 1: Generate Scoped Token using a credentials file path + try { + // Step 1: Specify the roles required for the scoped token + ArrayList roles = new ArrayList<>(); + roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID + + // Step 2: Specify the path to the credentials file + String filePath = ""; // Replace with the actual file path + + // Step 3: Create a BearerToken object using the file path and roles + BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Provide the credentials file + .setRoles(roles) // Set the roles for the scoped token + .build(); + + // Step 4: Generate and print the Scoped Bearer Token + scopedToken = bearerToken.getBearerToken(); + System.out.println("Scoped Token (using file path): " + scopedToken); + } catch (SkyflowException e) { // Handle exceptions during token generation + System.out.println("Error occurred while generating Scoped Token using file path:"); + e.printStackTrace(); + } + + // Example 2: Generate Scoped Token using credentials as a string + try { + // Step 1: Specify the roles required for the scoped token + ArrayList roles = new ArrayList<>(); + roles.add("YOUR_ROLE_ID"); // Replace with your actual role ID + + // Step 2: Specify the credentials as a string (file contents) + String fileContents = ""; // Replace with actual file contents + + // Step 3: Create a BearerToken object using the credentials string and roles + BearerToken bearerToken = BearerToken.builder() + .setCredentials(fileContents) // Provide the credentials as a string + .setRoles(roles) // Set the roles for the scoped token + .build(); + + // Step 4: Generate and print the Scoped Bearer Token + scopedToken = bearerToken.getBearerToken(); + System.out.println("Scoped Token (using credentials string): " + scopedToken); + } catch (SkyflowException e) { // Handle exceptions during token generation + System.out.println("Error occurred while generating Scoped Token using credentials string:"); + e.printStackTrace(); + } + } +} diff --git a/skyvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java b/skyvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java new file mode 100644 index 00000000..517d5cc8 --- /dev/null +++ b/skyvault/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java @@ -0,0 +1,89 @@ +package com.example.serviceaccount; + +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.SignedDataTokenResponse; +import com.skyflow.serviceaccount.util.SignedDataTokens; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This example demonstrates how to generate Signed Data Tokens using: + * 1. String context. + * 2. JSON object context (Map) for conditional data access policies. + * 3. Credentials string with context. + */ +public class SignedTokenGenerationExample { + public static void main(String[] args) { + List signedTokenValues; + + // Example 1: Signed data tokens with string context + try { + String filePath = ""; + String context = "user_12345"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx(context) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (string context): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Example 2: Signed data tokens with JSON object context + // Each key maps to a Skyflow CEL policy variable under request.context.* + // For example: request.context.role == "analyst" && request.context.department == "research" + try { + String filePath = ""; + Map ctx = new HashMap<>(); + ctx.put("role", "analyst"); + ctx.put("department", "research"); + ctx.put("user_id", "user_67890"); + + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx(ctx) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (object context): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + + // Example 3: Signed data tokens from credentials string + try { + String fileContents = ""; + String context = "user_12345"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("YOUR_DATA_TOKEN_1"); + + SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(fileContents) + .setCtx(context) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + + signedTokenValues = signedToken.getSignedDataTokens(); + System.out.println("Signed Tokens (creds string): " + signedTokenValues); + } catch (SkyflowException e) { + e.printStackTrace(); + } + } +} diff --git a/samples/src/main/java/com/example/vault/ClientOperations.java b/skyvault/samples/src/main/java/com/example/vault/ClientOperations.java similarity index 100% rename from samples/src/main/java/com/example/vault/ClientOperations.java rename to skyvault/samples/src/main/java/com/example/vault/ClientOperations.java diff --git a/samples/src/main/java/com/example/vault/CredentialsOptions.java b/skyvault/samples/src/main/java/com/example/vault/CredentialsOptions.java similarity index 100% rename from samples/src/main/java/com/example/vault/CredentialsOptions.java rename to skyvault/samples/src/main/java/com/example/vault/CredentialsOptions.java diff --git a/samples/src/main/java/com/example/vault/DeleteExample.java b/skyvault/samples/src/main/java/com/example/vault/DeleteExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/DeleteExample.java rename to skyvault/samples/src/main/java/com/example/vault/DeleteExample.java diff --git a/samples/src/main/java/com/example/vault/DetokenizeExample.java b/skyvault/samples/src/main/java/com/example/vault/DetokenizeExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/DetokenizeExample.java rename to skyvault/samples/src/main/java/com/example/vault/DetokenizeExample.java diff --git a/samples/src/main/java/com/example/vault/FileUploadExample.java b/skyvault/samples/src/main/java/com/example/vault/FileUploadExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/FileUploadExample.java rename to skyvault/samples/src/main/java/com/example/vault/FileUploadExample.java diff --git a/samples/src/main/java/com/example/vault/GetExample.java b/skyvault/samples/src/main/java/com/example/vault/GetExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/GetExample.java rename to skyvault/samples/src/main/java/com/example/vault/GetExample.java diff --git a/samples/src/main/java/com/example/vault/InsertExample.java b/skyvault/samples/src/main/java/com/example/vault/InsertExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/InsertExample.java rename to skyvault/samples/src/main/java/com/example/vault/InsertExample.java diff --git a/samples/src/main/java/com/example/vault/QueryExample.java b/skyvault/samples/src/main/java/com/example/vault/QueryExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/QueryExample.java rename to skyvault/samples/src/main/java/com/example/vault/QueryExample.java diff --git a/samples/src/main/java/com/example/vault/TokenizeExample.java b/skyvault/samples/src/main/java/com/example/vault/TokenizeExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/TokenizeExample.java rename to skyvault/samples/src/main/java/com/example/vault/TokenizeExample.java diff --git a/samples/src/main/java/com/example/vault/UpdateExample.java b/skyvault/samples/src/main/java/com/example/vault/UpdateExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/UpdateExample.java rename to skyvault/samples/src/main/java/com/example/vault/UpdateExample.java diff --git a/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java b/skyvault/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java rename to skyvault/samples/src/main/java/com/example/vault/deprecated/DetokenizeExample.java diff --git a/samples/src/main/java/com/example/vault/deprecated/GetExample.java b/skyvault/samples/src/main/java/com/example/vault/deprecated/GetExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/deprecated/GetExample.java rename to skyvault/samples/src/main/java/com/example/vault/deprecated/GetExample.java diff --git a/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java b/skyvault/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java similarity index 100% rename from samples/src/main/java/com/example/vault/deprecated/UpdateExample.java rename to skyvault/samples/src/main/java/com/example/vault/deprecated/UpdateExample.java diff --git a/v2/src/main/java/com/skyflow/ConnectionClient.java b/skyvault/src/main/java/com/skyflow/ConnectionClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/ConnectionClient.java rename to skyvault/src/main/java/com/skyflow/ConnectionClient.java diff --git a/v2/src/main/java/com/skyflow/Skyflow.java b/skyvault/src/main/java/com/skyflow/Skyflow.java similarity index 100% rename from v2/src/main/java/com/skyflow/Skyflow.java rename to skyvault/src/main/java/com/skyflow/Skyflow.java diff --git a/v2/src/main/java/com/skyflow/VaultClient.java b/skyvault/src/main/java/com/skyflow/VaultClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/VaultClient.java rename to skyvault/src/main/java/com/skyflow/VaultClient.java diff --git a/v2/src/main/java/com/skyflow/config/ConnectionConfig.java b/skyvault/src/main/java/com/skyflow/config/ConnectionConfig.java similarity index 100% rename from v2/src/main/java/com/skyflow/config/ConnectionConfig.java rename to skyvault/src/main/java/com/skyflow/config/ConnectionConfig.java diff --git a/v2/src/main/java/com/skyflow/config/ManagementConfig.java b/skyvault/src/main/java/com/skyflow/config/ManagementConfig.java similarity index 100% rename from v2/src/main/java/com/skyflow/config/ManagementConfig.java rename to skyvault/src/main/java/com/skyflow/config/ManagementConfig.java diff --git a/v2/src/main/java/com/skyflow/config/VaultConfig.java b/skyvault/src/main/java/com/skyflow/config/VaultConfig.java similarity index 100% rename from v2/src/main/java/com/skyflow/config/VaultConfig.java rename to skyvault/src/main/java/com/skyflow/config/VaultConfig.java diff --git a/v2/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java b/skyvault/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java rename to skyvault/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java diff --git a/v2/src/main/java/com/skyflow/enums/DetectEntities.java b/skyvault/src/main/java/com/skyflow/enums/DetectEntities.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/DetectEntities.java rename to skyvault/src/main/java/com/skyflow/enums/DetectEntities.java diff --git a/v2/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java b/skyvault/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java rename to skyvault/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java diff --git a/v2/src/main/java/com/skyflow/enums/Env.java b/skyvault/src/main/java/com/skyflow/enums/Env.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/Env.java rename to skyvault/src/main/java/com/skyflow/enums/Env.java diff --git a/v2/src/main/java/com/skyflow/enums/InterfaceName.java b/skyvault/src/main/java/com/skyflow/enums/InterfaceName.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/InterfaceName.java rename to skyvault/src/main/java/com/skyflow/enums/InterfaceName.java diff --git a/v2/src/main/java/com/skyflow/enums/LogLevel.java b/skyvault/src/main/java/com/skyflow/enums/LogLevel.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/LogLevel.java rename to skyvault/src/main/java/com/skyflow/enums/LogLevel.java diff --git a/v2/src/main/java/com/skyflow/enums/MaskingMethod.java b/skyvault/src/main/java/com/skyflow/enums/MaskingMethod.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/MaskingMethod.java rename to skyvault/src/main/java/com/skyflow/enums/MaskingMethod.java diff --git a/v2/src/main/java/com/skyflow/enums/RedactionType.java b/skyvault/src/main/java/com/skyflow/enums/RedactionType.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/RedactionType.java rename to skyvault/src/main/java/com/skyflow/enums/RedactionType.java diff --git a/v2/src/main/java/com/skyflow/enums/RequestMethod.java b/skyvault/src/main/java/com/skyflow/enums/RequestMethod.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/RequestMethod.java rename to skyvault/src/main/java/com/skyflow/enums/RequestMethod.java diff --git a/v2/src/main/java/com/skyflow/enums/TokenMode.java b/skyvault/src/main/java/com/skyflow/enums/TokenMode.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/TokenMode.java rename to skyvault/src/main/java/com/skyflow/enums/TokenMode.java diff --git a/v2/src/main/java/com/skyflow/enums/TokenType.java b/skyvault/src/main/java/com/skyflow/enums/TokenType.java similarity index 100% rename from v2/src/main/java/com/skyflow/enums/TokenType.java rename to skyvault/src/main/java/com/skyflow/enums/TokenType.java diff --git a/v2/src/main/java/com/skyflow/errors/ErrorMessage.java b/skyvault/src/main/java/com/skyflow/errors/ErrorMessage.java similarity index 100% rename from v2/src/main/java/com/skyflow/errors/ErrorMessage.java rename to skyvault/src/main/java/com/skyflow/errors/ErrorMessage.java diff --git a/v2/src/main/java/com/skyflow/errors/SkyflowException.java b/skyvault/src/main/java/com/skyflow/errors/SkyflowException.java similarity index 100% rename from v2/src/main/java/com/skyflow/errors/SkyflowException.java rename to skyvault/src/main/java/com/skyflow/errors/SkyflowException.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/ApiClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/ApiClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/ApiClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/ApiClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java b/skyvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java rename to skyvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java b/skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java rename to skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/Environment.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Environment.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/Environment.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/Environment.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/FileStream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/FileStream.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/Nullable.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/Nullable.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/Stream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Stream.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/Stream.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/Stream.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/core/Suppliers.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/core/Suppliers.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/errors/BadRequestError.java b/skyvault/src/main/java/com/skyflow/generated/rest/errors/BadRequestError.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/errors/BadRequestError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/errors/BadRequestError.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/errors/InternalServerError.java b/skyvault/src/main/java/com/skyflow/generated/rest/errors/InternalServerError.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/errors/InternalServerError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/errors/InternalServerError.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/errors/NotFoundError.java b/skyvault/src/main/java/com/skyflow/generated/rest/errors/NotFoundError.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/errors/NotFoundError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/errors/NotFoundError.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/errors/UnauthorizedError.java b/skyvault/src/main/java/com/skyflow/generated/rest/errors/UnauthorizedError.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/errors/UnauthorizedError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/errors/UnauthorizedError.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/AuditClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AuditClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/AuditClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AuditClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/requests/AuditServiceListAuditEventsRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/requests/AuditServiceListAuditEventsRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/requests/AuditServiceListAuditEventsRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/requests/AuditServiceListAuditEventsRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsActionType.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsActionType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsActionType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsActionType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAccessType.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAccessType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAccessType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAccessType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextActorType.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextActorType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextActorType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextActorType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAuthMode.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAuthMode.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAuthMode.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsContextAuthMode.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsResourceType.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsResourceType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsResourceType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestFilterOpsResourceType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestSortOpsOrderBy.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestSortOpsOrderBy.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestSortOpsOrderBy.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/types/AuditServiceListAuditEventsRequestSortOpsOrderBy.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/BinLookupClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/BinLookupClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/BinLookupClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/BinLookupClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/requests/V1BinListRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/requests/V1BinListRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/binlookup/requests/V1BinListRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/requests/V1BinListRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileAudioRequestDeidentifyAudio.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileAudioRequestDeidentifyAudio.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileAudioRequestDeidentifyAudio.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileAudioRequestDeidentifyAudio.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileDocumentPdfRequestDeidentifyPdf.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileDocumentPdfRequestDeidentifyPdf.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileDocumentPdfRequestDeidentifyPdf.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileDocumentPdfRequestDeidentifyPdf.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileImageRequestDeidentifyImage.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileImageRequestDeidentifyImage.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileImageRequestDeidentifyImage.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileImageRequestDeidentifyImage.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyDocument.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyDocument.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyDocument.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyDocument.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyPresentation.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyPresentation.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyPresentation.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyPresentation.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifySpreadsheet.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifySpreadsheet.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifySpreadsheet.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifySpreadsheet.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyStructuredText.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyStructuredText.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyStructuredText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyStructuredText.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyText.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyText.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/DeidentifyFileRequestDeidentifyText.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/GetRunRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/GetRunRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/GetRunRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/GetRunRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/ReidentifyFileRequestReidentifyFile.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/ReidentifyFileRequestReidentifyFile.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/requests/ReidentifyFileRequestReidentifyFile.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/requests/ReidentifyFileRequestReidentifyFile.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioOutputTranscription.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioOutputTranscription.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioOutputTranscription.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileAudioRequestDeidentifyAudioOutputTranscription.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageMaskingMethod.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageMaskingMethod.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageMaskingMethod.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileImageRequestDeidentifyImageMaskingMethod.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyDocumentEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyDocumentEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyDocumentEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyDocumentEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyPresentationEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyPresentationEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyPresentationEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyPresentationEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyTextEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyTextEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyTextEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestDeidentifyTextEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/files/types/DeidentifyFileRequestEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/GuardrailsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/GuardrailsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/GuardrailsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/GuardrailsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/requests/DetectGuardrailsRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/requests/DetectGuardrailsRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/guardrails/requests/DetectGuardrailsRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/requests/DetectGuardrailsRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/query/QueryClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/QueryClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/query/QueryClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/query/QueryClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/query/requests/QueryServiceExecuteQueryBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/requests/QueryServiceExecuteQueryBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/query/requests/QueryServiceExecuteQueryBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/query/requests/QueryServiceExecuteQueryBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/FileServiceUploadFileRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/FileServiceUploadFileRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/FileServiceUploadFileRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/FileServiceUploadFileRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBatchOperationBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBatchOperationBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBatchOperationBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBatchOperationBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkDeleteRecordBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkDeleteRecordBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkDeleteRecordBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkDeleteRecordBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkGetRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkGetRecordRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkGetRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceBulkGetRecordRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceGetRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceGetRecordRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceGetRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceGetRecordRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceInsertRecordBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceInsertRecordBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceInsertRecordBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceInsertRecordBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceUpdateRecordBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceUpdateRecordBody.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceUpdateRecordBody.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/RecordServiceUpdateRecordBody.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/UploadFileV2Request.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/UploadFileV2Request.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/requests/UploadFileV2Request.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/UploadFileV2Request.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestOrderBy.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestOrderBy.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestOrderBy.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestOrderBy.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestRedaction.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestRedaction.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestRedaction.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceBulkGetRecordRequestRedaction.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceGetRecordRequestRedaction.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceGetRecordRequestRedaction.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceGetRecordRequestRedaction.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/records/types/RecordServiceGetRecordRequestRedaction.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/StringsClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/StringsClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/StringsClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/StringsClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/requests/DeidentifyStringRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/requests/DeidentifyStringRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/requests/DeidentifyStringRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/requests/DeidentifyStringRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/requests/ReidentifyStringRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/requests/ReidentifyStringRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/requests/ReidentifyStringRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/requests/ReidentifyStringRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/strings/types/DeidentifyStringRequestEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/types/DeidentifyStringRequestEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/strings/types/DeidentifyStringRequestEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/types/DeidentifyStringRequestEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/TokensClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/TokensClient.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/TokensClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/TokensClient.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1DetokenizePayload.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1DetokenizePayload.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1DetokenizePayload.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1DetokenizePayload.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1TokenizePayload.java b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1TokenizePayload.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1TokenizePayload.java rename to skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/requests/V1TokenizePayload.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/AuditEventAuditResourceType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventAuditResourceType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/AuditEventAuditResourceType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventAuditResourceType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/BatchRecordMethod.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/BatchRecordMethod.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/BatchRecordMethod.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/BatchRecordMethod.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ContextAccessType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ContextAccessType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ContextAccessType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ContextAccessType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ContextAuthMode.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ContextAuthMode.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ContextAuthMode.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ContextAuthMode.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileExtension.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileExtension.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileExtension.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileExtension.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutputProcessedFileType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponseValidation.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponseValidation.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponseValidation.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponseValidation.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseOutputType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseOutputType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseOutputType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseOutputType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseStatus.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseStatus.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseStatus.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponseStatus.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/DetokenizeRecordResponseValueType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetokenizeRecordResponseValueType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/DetokenizeRecordResponseValueType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetokenizeRecordResponseValueType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileData.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileData.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileData.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileData.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudioDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudioDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudioDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudioDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocumentDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocumentDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocumentDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocumentDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImageDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImageDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImageDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImageDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentationDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentationDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentationDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentationDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheetDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheetDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheetDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheetDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredTextDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredTextDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredTextDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredTextDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFileDataFormat.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFileDataFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFileDataFormat.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFileDataFormat.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/Format.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Format.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/Format.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Format.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FormatMaskedItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FormatMaskedItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FormatMaskedItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FormatMaskedItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FormatPlaintextItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FormatPlaintextItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FormatPlaintextItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FormatPlaintextItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/FormatRedactedItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FormatRedactedItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/FormatRedactedItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FormatRedactedItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/Locations.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Locations.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/Locations.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Locations.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/RedactionEnumRedaction.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/RedactionEnumRedaction.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/RedactionEnumRedaction.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/RedactionEnumRedaction.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutputProcessedFileExtension.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutputProcessedFileExtension.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutputProcessedFileExtension.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutputProcessedFileExtension.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseOutputType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseOutputType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseOutputType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseOutputType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseStatus.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseStatus.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseStatus.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponseStatus.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/RequestActionType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/RequestActionType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/RequestActionType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/RequestActionType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/ShiftDatesEntityTypesItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDatesEntityTypesItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/ShiftDatesEntityTypesItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDatesEntityTypesItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingDefault.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingDefault.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingDefault.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingDefault.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityOnlyItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityOnlyItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityOnlyItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityOnlyItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityUnqCounterItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityUnqCounterItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityUnqCounterItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingEntityUnqCounterItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingVaultTokenItem.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingVaultTokenItem.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingVaultTokenItem.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMappingVaultTokenItem.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/Transformations.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Transformations.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/Transformations.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Transformations.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1Byot.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1Byot.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1Byot.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1Byot.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1Card.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1Card.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1Card.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1Card.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1FileAvScanStatus.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1FileAvScanStatus.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1FileAvScanStatus.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1FileAvScanStatus.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1MemberType.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1MemberType.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1MemberType.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1MemberType.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java diff --git a/v2/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java similarity index 100% rename from v2/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java diff --git a/v2/src/main/java/com/skyflow/utils/Constants.java b/skyvault/src/main/java/com/skyflow/utils/Constants.java similarity index 100% rename from v2/src/main/java/com/skyflow/utils/Constants.java rename to skyvault/src/main/java/com/skyflow/utils/Constants.java diff --git a/v2/src/main/java/com/skyflow/utils/HttpUtility.java b/skyvault/src/main/java/com/skyflow/utils/HttpUtility.java similarity index 100% rename from v2/src/main/java/com/skyflow/utils/HttpUtility.java rename to skyvault/src/main/java/com/skyflow/utils/HttpUtility.java diff --git a/v2/src/main/java/com/skyflow/utils/Utils.java b/skyvault/src/main/java/com/skyflow/utils/Utils.java similarity index 100% rename from v2/src/main/java/com/skyflow/utils/Utils.java rename to skyvault/src/main/java/com/skyflow/utils/Utils.java diff --git a/v2/src/main/java/com/skyflow/utils/logger/LogUtil.java b/skyvault/src/main/java/com/skyflow/utils/logger/LogUtil.java similarity index 100% rename from v2/src/main/java/com/skyflow/utils/logger/LogUtil.java rename to skyvault/src/main/java/com/skyflow/utils/logger/LogUtil.java diff --git a/v2/src/main/java/com/skyflow/utils/validations/Validations.java b/skyvault/src/main/java/com/skyflow/utils/validations/Validations.java similarity index 100% rename from v2/src/main/java/com/skyflow/utils/validations/Validations.java rename to skyvault/src/main/java/com/skyflow/utils/validations/Validations.java diff --git a/v2/src/main/java/com/skyflow/vault/audit/ListEventRequest.java b/skyvault/src/main/java/com/skyflow/vault/audit/ListEventRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/audit/ListEventRequest.java rename to skyvault/src/main/java/com/skyflow/vault/audit/ListEventRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/audit/ListEventResponse.java b/skyvault/src/main/java/com/skyflow/vault/audit/ListEventResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/audit/ListEventResponse.java rename to skyvault/src/main/java/com/skyflow/vault/audit/ListEventResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/bin/GetBinRequest.java b/skyvault/src/main/java/com/skyflow/vault/bin/GetBinRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/bin/GetBinRequest.java rename to skyvault/src/main/java/com/skyflow/vault/bin/GetBinRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/bin/GetBinResponse.java b/skyvault/src/main/java/com/skyflow/vault/bin/GetBinResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/bin/GetBinResponse.java rename to skyvault/src/main/java/com/skyflow/vault/bin/GetBinResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java b/skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java rename to skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java b/skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java rename to skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/controller/AuditController.java b/skyvault/src/main/java/com/skyflow/vault/controller/AuditController.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/controller/AuditController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/AuditController.java diff --git a/v2/src/main/java/com/skyflow/vault/controller/BinLookupController.java b/skyvault/src/main/java/com/skyflow/vault/controller/BinLookupController.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/controller/BinLookupController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/BinLookupController.java diff --git a/v2/src/main/java/com/skyflow/vault/controller/ConnectionController.java b/skyvault/src/main/java/com/skyflow/vault/controller/ConnectionController.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/controller/ConnectionController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/ConnectionController.java diff --git a/v2/src/main/java/com/skyflow/vault/controller/DetectController.java b/skyvault/src/main/java/com/skyflow/vault/controller/DetectController.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/controller/DetectController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/DetectController.java diff --git a/v2/src/main/java/com/skyflow/vault/controller/VaultController.java b/skyvault/src/main/java/com/skyflow/vault/controller/VaultController.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/controller/VaultController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/VaultController.java diff --git a/v2/src/main/java/com/skyflow/vault/data/DeleteRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/DeleteRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/DeleteResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/DeleteResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/data/FileUploadRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/FileUploadRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/FileUploadRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/FileUploadRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/FileUploadResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/FileUploadResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/FileUploadResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/FileUploadResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/data/GetRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/GetRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/GetRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/GetRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/GetResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/GetResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/GetResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/GetResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/data/InsertRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/InsertRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/InsertRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/InsertRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/InsertResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/InsertResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/InsertResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/InsertResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/data/QueryRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/QueryRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/QueryRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/QueryRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/QueryResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/QueryResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/QueryResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/QueryResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/data/UpdateRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/UpdateRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/data/UpdateResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/data/UpdateResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/AudioBleep.java b/skyvault/src/main/java/com/skyflow/vault/detect/AudioBleep.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/AudioBleep.java rename to skyvault/src/main/java/com/skyflow/vault/detect/AudioBleep.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/DateTransformation.java b/skyvault/src/main/java/com/skyflow/vault/detect/DateTransformation.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/DateTransformation.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DateTransformation.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java b/skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java b/skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java b/skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java b/skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/EntityInfo.java b/skyvault/src/main/java/com/skyflow/vault/detect/EntityInfo.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/EntityInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/EntityInfo.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/FileEntityInfo.java b/skyvault/src/main/java/com/skyflow/vault/detect/FileEntityInfo.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/FileEntityInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileEntityInfo.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/FileInfo.java b/skyvault/src/main/java/com/skyflow/vault/detect/FileInfo.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/FileInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileInfo.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/FileInput.java b/skyvault/src/main/java/com/skyflow/vault/detect/FileInput.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/FileInput.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileInput.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java b/skyvault/src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java b/skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java b/skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/TextIndex.java b/skyvault/src/main/java/com/skyflow/vault/detect/TextIndex.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/TextIndex.java rename to skyvault/src/main/java/com/skyflow/vault/detect/TextIndex.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/TokenFormat.java b/skyvault/src/main/java/com/skyflow/vault/detect/TokenFormat.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/TokenFormat.java rename to skyvault/src/main/java/com/skyflow/vault/detect/TokenFormat.java diff --git a/v2/src/main/java/com/skyflow/vault/detect/Transformations.java b/skyvault/src/main/java/com/skyflow/vault/detect/Transformations.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/detect/Transformations.java rename to skyvault/src/main/java/com/skyflow/vault/detect/Transformations.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/ColumnValue.java b/skyvault/src/main/java/com/skyflow/vault/tokens/ColumnValue.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/ColumnValue.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/ColumnValue.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java b/skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java diff --git a/v2/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java similarity index 100% rename from v2/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java diff --git a/v2/src/main/resources/sdk.properties b/skyvault/src/main/resources/sdk.properties similarity index 100% rename from v2/src/main/resources/sdk.properties rename to skyvault/src/main/resources/sdk.properties diff --git a/v2/src/test/java/com/skyflow/ConnectionClientDotenvTests.java b/skyvault/src/test/java/com/skyflow/ConnectionClientDotenvTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/ConnectionClientDotenvTests.java rename to skyvault/src/test/java/com/skyflow/ConnectionClientDotenvTests.java diff --git a/v2/src/test/java/com/skyflow/ConnectionClientTests.java b/skyvault/src/test/java/com/skyflow/ConnectionClientTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/ConnectionClientTests.java rename to skyvault/src/test/java/com/skyflow/ConnectionClientTests.java diff --git a/v2/src/test/java/com/skyflow/RequestFidelityTests.java b/skyvault/src/test/java/com/skyflow/RequestFidelityTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/RequestFidelityTests.java rename to skyvault/src/test/java/com/skyflow/RequestFidelityTests.java diff --git a/v2/src/test/java/com/skyflow/SkyflowTests.java b/skyvault/src/test/java/com/skyflow/SkyflowTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/SkyflowTests.java rename to skyvault/src/test/java/com/skyflow/SkyflowTests.java diff --git a/v2/src/test/java/com/skyflow/VaultClientTests.java b/skyvault/src/test/java/com/skyflow/VaultClientTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/VaultClientTests.java rename to skyvault/src/test/java/com/skyflow/VaultClientTests.java diff --git a/v2/src/test/java/com/skyflow/config/ConnectionConfigTests.java b/skyvault/src/test/java/com/skyflow/config/ConnectionConfigTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/config/ConnectionConfigTests.java rename to skyvault/src/test/java/com/skyflow/config/ConnectionConfigTests.java diff --git a/v2/src/test/java/com/skyflow/config/ManagementConfigTest.java b/skyvault/src/test/java/com/skyflow/config/ManagementConfigTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/config/ManagementConfigTest.java rename to skyvault/src/test/java/com/skyflow/config/ManagementConfigTest.java diff --git a/v2/src/test/java/com/skyflow/config/VaultConfigTests.java b/skyvault/src/test/java/com/skyflow/config/VaultConfigTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/config/VaultConfigTests.java rename to skyvault/src/test/java/com/skyflow/config/VaultConfigTests.java diff --git a/v2/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java b/skyvault/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java rename to skyvault/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java diff --git a/v2/src/test/java/com/skyflow/enums/DetectEntitiesTest.java b/skyvault/src/test/java/com/skyflow/enums/DetectEntitiesTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/DetectEntitiesTest.java rename to skyvault/src/test/java/com/skyflow/enums/DetectEntitiesTest.java diff --git a/v2/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java b/skyvault/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java rename to skyvault/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java diff --git a/v2/src/test/java/com/skyflow/enums/MaskingMethodTest.java b/skyvault/src/test/java/com/skyflow/enums/MaskingMethodTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/MaskingMethodTest.java rename to skyvault/src/test/java/com/skyflow/enums/MaskingMethodTest.java diff --git a/v2/src/test/java/com/skyflow/enums/TokenModeTest.java b/skyvault/src/test/java/com/skyflow/enums/TokenModeTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/TokenModeTest.java rename to skyvault/src/test/java/com/skyflow/enums/TokenModeTest.java diff --git a/v2/src/test/java/com/skyflow/enums/TokenTypeTest.java b/skyvault/src/test/java/com/skyflow/enums/TokenTypeTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/enums/TokenTypeTest.java rename to skyvault/src/test/java/com/skyflow/enums/TokenTypeTest.java diff --git a/v2/src/test/java/com/skyflow/utils/HttpUtilityTests.java b/skyvault/src/test/java/com/skyflow/utils/HttpUtilityTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/utils/HttpUtilityTests.java rename to skyvault/src/test/java/com/skyflow/utils/HttpUtilityTests.java diff --git a/v2/src/test/java/com/skyflow/utils/UtilsTests.java b/skyvault/src/test/java/com/skyflow/utils/UtilsTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/utils/UtilsTests.java rename to skyvault/src/test/java/com/skyflow/utils/UtilsTests.java diff --git a/v2/src/test/java/com/skyflow/vault/BinAuditTests.java b/skyvault/src/test/java/com/skyflow/vault/BinAuditTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/BinAuditTests.java rename to skyvault/src/test/java/com/skyflow/vault/BinAuditTests.java diff --git a/v2/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java b/skyvault/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java rename to skyvault/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java diff --git a/v2/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java diff --git a/v2/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java diff --git a/v2/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java diff --git a/v2/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java diff --git a/v2/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/DeleteTests.java b/skyvault/src/test/java/com/skyflow/vault/data/DeleteTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/DeleteTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/DeleteTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/FileUploadTests.java b/skyvault/src/test/java/com/skyflow/vault/data/FileUploadTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/FileUploadTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/FileUploadTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/GetTests.java b/skyvault/src/test/java/com/skyflow/vault/data/GetTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/GetTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/GetTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/InsertTests.java b/skyvault/src/test/java/com/skyflow/vault/data/InsertTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/InsertTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/InsertTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/QueryResponseTest.java b/skyvault/src/test/java/com/skyflow/vault/data/QueryResponseTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/QueryResponseTest.java rename to skyvault/src/test/java/com/skyflow/vault/data/QueryResponseTest.java diff --git a/v2/src/test/java/com/skyflow/vault/data/QueryTests.java b/skyvault/src/test/java/com/skyflow/vault/data/QueryTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/QueryTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/QueryTests.java diff --git a/v2/src/test/java/com/skyflow/vault/data/UpdateTests.java b/skyvault/src/test/java/com/skyflow/vault/data/UpdateTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/data/UpdateTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/UpdateTests.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java b/skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java b/skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java b/skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java b/skyvault/src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/FileInfoTest.java b/skyvault/src/test/java/com/skyflow/vault/detect/FileInfoTest.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/FileInfoTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/FileInfoTest.java diff --git a/v2/src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java b/skyvault/src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java rename to skyvault/src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java diff --git a/v2/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java b/skyvault/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java rename to skyvault/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java diff --git a/v2/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java b/skyvault/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java similarity index 100% rename from v2/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java rename to skyvault/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java diff --git a/v2/src/test/resources/notJson.txt b/skyvault/src/test/resources/notJson.txt similarity index 100% rename from v2/src/test/resources/notJson.txt rename to skyvault/src/test/resources/notJson.txt diff --git a/v2/api-report/skyflow-java.baseline.jar b/v2/api-report/skyflow-java.baseline.jar deleted file mode 100644 index c35c4ef7..00000000 Binary files a/v2/api-report/skyflow-java.baseline.jar and /dev/null differ