diff --git a/.cspell.json b/.cspell.json index 757f0c48..58d8ce26 100644 --- a/.cspell.json +++ b/.cspell.json @@ -6,6 +6,7 @@ "**/*.{java,md,xml,yml,yaml,json,txt,properties}" ], "words": [ + "japicmp", "Skyflow", "skyflow", "skyflowapi", @@ -96,9 +97,20 @@ "nocreds", "nodir", "detok", - "qhdmceurtnlz", "ngrok", - "obac" + "obac", + "siom", + "vaultid", + "recordss", + "synthesise", + "synthesised", + "deserialise", + "deserialised", + "unmodelled", + "recordss", + "rarr", + "servname", + "nodename" ], "languageSettings": [ { @@ -118,7 +130,8 @@ "**/target/**", "*.lock", "Rule/**", - "src/main/java/com/skyflow/generated/**", + "**/src/main/java/com/skyflow/generated/**", + "**/generated/**", "**/*.ts", "**/processed-*", "samples/src/main/java/com/example/credentials.json", diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml deleted file mode 100644 index 13fcf623..00000000 --- a/.github/workflows/beta-release.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Public beta release -on: - push: - tags: '*.*.*-beta.*' -jobs: - build-and-deploy: - uses: ./.github/workflows/shared-build-and-deploy.yml - with: - ref: ${{ github.ref_name }} - server-id: central - profile: maven-central - tag: 'beta' - secrets: - server-username: ${{ secrets.CENTRAL_PUBLISHER_PORTAL_USERNAME }} - server-password: ${{ secrets.CENTRAL_PUBLISHER_PORTAL_PASSWORD }} - gpg-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} - gpg-passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - skyflow-credentials: ${{ secrets.SKYFLOW_CREDENTIALS }} >> .env - test-expired-token: ${{ secrets.TEST_EXPIRED_TOKEN }} >> .env - test-reusable-token: ${{ secrets.TEST_REUSABLE_TOKEN }} >> .env diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml new file mode 100644 index 00000000..9a3c26f7 --- /dev/null +++ b/.github/workflows/contract-tests.yml @@ -0,0 +1,166 @@ +name: Contract Tests + +on: + pull_request: + branches: + - main + - release/* + - flowvault-release/* + +jobs: + contract-tests: + # One job per module so a break in one is reported against that module by name, + # and both still run even when the other fails. + name: Contract Tests (${{ matrix.module }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - module: skyvault + artifact: skyflow-java + - module: flowvault + artifact: skyflow-flowvault-java + + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + cache: 'maven' + + - name: Verify API surface snapshot + run: mvn -B install -pl common,${{ matrix.module }} -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true + + - name: Show API surface diff + if: failure() + run: | + echo "### API surface changes detected in ${{ matrix.module }} ###" + echo "Compared against ${{ matrix.module }}/api-report/${{ matrix.artifact }}.baseline.jar." + echo "If this change is intentional, run:" + echo " scripts/contract-snapshot-update.sh ${{ matrix.module }}" + echo "and commit the updated baseline jar." + echo "" + cat ${{ matrix.module }}/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-${{ matrix.module }} + path: ${{ matrix.module }}/target/japicmp/** + retention-days: 7 + + # The step above only shows a diff when the CURRENT build differs from the + # committed baseline - once someone runs contract-snapshot-update.sh and + # commits the refreshed baseline jar, that check goes green and shows nothing. + # A reviewer looking at a green PR that touches api-report/*.baseline.jar + # (a binary file) would otherwise have no way to see WHAT was just approved as + # the new contract. These steps explicitly diff the OLD committed baseline + # (from the PR's base branch) against the NEW committed baseline (from this PR) + # and post it as a PR comment, regardless of whether the check above passed. + - name: Check if contract baseline was updated in this PR + id: baseline-diff-check + if: always() && github.event.pull_request + run: | + git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + BASELINE="${{ matrix.module }}/api-report/${{ matrix.artifact }}.baseline.jar" + if ! git diff --name-only "origin/${{ github.event.pull_request.base.ref }}" HEAD -- "$BASELINE" | grep -q .; then + echo "changed=false" >> "$GITHUB_OUTPUT" + elif git cat-file -e "origin/${{ github.event.pull_request.base.ref }}:$BASELINE" 2>/dev/null; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + # Added by this PR rather than modified: the module is getting its + # first baseline. git diff reports an addition as a change, but there + # is no old snapshot to `git show`, so a plain "true" here would send + # the next step into `git show :` and exit 128. + echo "changed=new" >> "$GITHUB_OUTPUT" + fi + + - name: Diff old vs new contract baseline + if: always() && (steps.baseline-diff-check.outputs.changed == 'true' || steps.baseline-diff-check.outputs.changed == 'new') + run: | + BASELINE="${{ matrix.module }}/api-report/${{ matrix.artifact }}.baseline.jar" + + if [ "${{ steps.baseline-diff-check.outputs.changed }}" = "new" ]; then + { + echo "\`$BASELINE\` is **new in this PR** - \`${{ matrix.module }}\` had no committed baseline before, so there is nothing to diff against." + echo "" + echo "This snapshot becomes the approved contract: every later PR is compared against it, and any incompatible change fails the \`Contract Tests (${{ matrix.module }})\` job until someone regenerates it deliberately. Review it as the starting point, not as a change." + } > /tmp/contract-baseline-diff.md + cat /tmp/contract-baseline-diff.md + exit 0 + fi + + 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 ${{ matrix.module }} -Dmdep.outputFile=/tmp/module-classpath.txt -Dmaven.javadoc.skip=true -Dgpg.skip=true + git show "origin/${{ github.event.pull_request.base.ref }}:$BASELINE" > /tmp/old-baseline.jar + + # Same allowlist the poms gate on, so the comment shows the contract and + # nothing else. Keep these in sync with the in the module poms. + java -jar /tmp/japicmp-cli.jar \ + -o /tmp/old-baseline.jar \ + -n "$BASELINE" \ + -a protected \ + -i "com.skyflow.Skyflow;com.skyflow.config;com.skyflow.enums;com.skyflow.errors;com.skyflow.serviceaccount.util;com.skyflow.vault.audit;com.skyflow.vault.bin;com.skyflow.vault.connection;com.skyflow.vault.controller;com.skyflow.vault.data;com.skyflow.vault.detect;com.skyflow.vault.tokens" \ + --old-classpath "$(cat /tmp/module-classpath.txt)" \ + --new-classpath "$(cat /tmp/module-classpath.txt)" \ + -m \ + --ignore-missing-classes \ + --markdown > /tmp/contract-baseline-diff.md || true + + cat /tmp/contract-baseline-diff.md + + - name: Comment contract baseline change on PR + if: always() && (steps.baseline-diff-check.outputs.changed == 'true' || steps.baseline-diff-check.outputs.changed == 'new') + uses: actions/github-script@v7 + env: + BASELINE_STATE: ${{ steps.baseline-diff-check.outputs.changed }} + with: + script: | + const fs = require('fs'); + const module = '${{ matrix.module }}'; + const artifact = '${{ matrix.artifact }}'; + const summary = fs.readFileSync('/tmp/contract-baseline-diff.md', 'utf8'); + // per-module marker so the two matrix jobs update their own comment + const marker = ``; + const isNew = process.env.BASELINE_STATE === 'new'; + const heading = isNew + ? `## Contract baseline added (\`${module}\`)` + : `## Contract baseline change detected (\`${module}\`)`; + const preamble = isNew + ? `This PR adds \`${module}/api-report/${artifact}.baseline.jar\`, the approved public API contract for this module.` + : `This PR updates \`${module}/api-report/${artifact}.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:`; + const body = `${marker}\n${heading}\n\n${preamble}\n\n${summary}`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.github/workflows/endorlabsScan.yml b/.github/workflows/endorlabsScan.yml index 3c316417..762b742e 100644 --- a/.github/workflows/endorlabsScan.yml +++ b/.github/workflows/endorlabsScan.yml @@ -36,6 +36,15 @@ jobs: name: "credentials.json" json: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} + - name: Distribute test fixtures to modules + 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 skyvault flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - name: Compile Package run: mvn -B package -f pom.xml -Dmaven.javadoc.skip=true diff --git a/.github/workflows/internal-release.yml b/.github/workflows/internal-release.yml index e8b8ee61..808cc7e4 100644 --- a/.github/workflows/internal-release.yml +++ b/.github/workflows/internal-release.yml @@ -1,26 +1,62 @@ -name: Publish package to the JFROG Artifactory +name: Publish module to the JFROG Artifactory on: push: + # '**' not '*.*': Actions glob '*' does not match '/', so '*.*' let slash + # tags (flowvault/v1.0.0) through and fired this branch-only workflow. tags-ignore: - - '*.*' + - '**' paths-ignore: - "*.md" branches: + - flowvault-release/* + - skyvault-release/* + # Legacy: predates the per-module naming, still maps to skyvault. - release/* jobs: + resolve-module: + runs-on: ubuntu-latest + # Skip our own bump commit, or this loops: bump -> push -> release -> bump. + # PAT-authenticated pushes DO trigger workflows; GITHUB_TOKEN pushes do not. + # build-and-deploy needs this job, so skipping here skips the run. + if: ${{ !contains(github.event.head_commit.message, '[AUTOMATED]') }} + outputs: + module: ${{ steps.set-module.outputs.module }} + steps: + # Explicit match, no catch-all: defaulting once published the wrong module. + - name: Resolve module from branch name + id: set-module + env: + BRANCH: ${{ github.ref_name }} + run: | + case "$BRANCH" in + flowvault-release/*) MODULE="flowvault" ;; + skyvault-release/*) MODULE="skyvault" ;; + release/*) MODULE="skyvault" ;; + *) + echo "::error::Branch '$BRANCH' does not map to a module." + exit 1 + ;; + esac + echo "Branch '$BRANCH' -> module '$MODULE'" + echo "module=$MODULE" >> "$GITHUB_OUTPUT" + build-and-deploy: + needs: resolve-module uses: ./.github/workflows/shared-build-and-deploy.yml with: ref: ${{ github.ref_name }} server-id: central profile: jfrog tag: 'internal' + module: ${{ needs.resolve-module.outputs.module }} secrets: server-username: ${{ secrets.ARTIFACTORY_USERNAME }} server-password: ${{ secrets.ARTIFACTORY_PASSWORD }} gpg-key: ${{ secrets.JFROG_GPG_KEY }} gpg-passphrase: ${{ secrets.JFROG_GPG_PASSPHRASE }} - skyflow-credentials: ${{ secrets.SKYFLOW_CREDENTIALS }} >> .env - test-expired-token: ${{ secrets.TEST_EXPIRED_TOKEN }} >> .env - test-reusable-token: ${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + skyflow-credentials: ${{ secrets.SKYFLOW_CREDENTIALS }} + test-expired-token: ${{ secrets.TEST_EXPIRED_TOKEN }} + test-reusable-token: ${{ secrets.TEST_REUSABLE_TOKEN }} + pat-actions: ${{ secrets.PAT_ACTIONS }} + test-credentials-file-string: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4386de84..5489eeea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,13 +30,65 @@ jobs: echo TEST_EXPIRED_TOKEN=${{ secrets.TEST_EXPIRED_TOKEN }} >> .env echo TEST_REUSABLE_TOKEN=${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + - name: Distribute test fixtures to modules + 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 skyvault flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - 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-flowvault.yml b/.github/workflows/pr-flowvault.yml new file mode 100644 index 00000000..d15c89e8 --- /dev/null +++ b/.github/workflows/pr-flowvault.yml @@ -0,0 +1,86 @@ +name: PR CI Checks (flowvault) + +# flowvault is a folder under main, alongside skyvault - not a branch. +# This workflow fires for PRs targeting main or a flowvault-release/* branch +# that actually touch flowvault or its common dependency, and only builds/tests +# those two modules. skyvault is covered by pr.yml, not here. + +on: + pull_request: + branches: [ "main", "flowvault-release/**" ] + paths: + - "flowvault/**" + - "common/**" + - "pom.xml" + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "11" + cache: "maven" + + # package (not verify/install) deliberately stops short of any japicmp + # contract gate bound to a module's `verify` phase - that's a separate + # check. This job only proves flowvault (and common) compile and package. + - name: Build flowvault + run: | + mvn -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ + clean package -pl flowvault -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true + + test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "11" + cache: "maven" + + - name: create-json + id: create-json + uses: jsdaniell/create-json@1.1.2 + with: + name: "credentials.json" + json: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} + + - name: create env + id: create-env + run: | + for dir in common flowvault; do + if [ -f "$dir/pom.xml" ]; then + cp credentials.json "$dir/credentials.json" + { + echo "SKYFLOW_CREDENTIALS=${{ secrets.SKYFLOW_CREDENTIALS }}" + echo "TEST_EXPIRED_TOKEN=${{ secrets.TEST_EXPIRED_TOKEN }}" + echo "TEST_REUSABLE_TOKEN=${{ secrets.TEST_REUSABLE_TOKEN }}" + } >> "$dir/.env" + fi + done + + # jacoco:report is already bound to the `test` phase in the root pom + # (prepare-agent + report executions), so `mvn test` alone regenerates + # coverage - no need to invoke jacoco:report again on the command line. + - name: Run flowvault unit tests + run: | + mvn -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn \ + clean test -pl flowvault -am -Dmaven.javadoc.skip=true -Dgpg.skip=true + + - name: Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: flowvault/target/site/jacoco/jacoco.xml + flags: unittests-flowvault + name: codecov-skyflow-java-flowvault + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bb89e32f..822b1288 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -39,16 +39,68 @@ jobs: echo TEST_EXPIRED_TOKEN=${{ secrets.TEST_EXPIRED_TOKEN }} >> .env echo TEST_REUSABLE_TOKEN=${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + - name: Distribute test fixtures to modules + 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 skyvault flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + - 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 spellcheck: name: Run spellcheck diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 720e16d8..c1cb280d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,82 @@ name: Public release + +# Triggered by publishing a GitHub Release, not a raw tag push: the Release +# carries both facts needed here - target_commitish (the branch picked in the +# UI; a tag records only a commit) and tag_name (module prefix + version). +# +# Beta and final share this workflow - 'release' events cannot be filtered by +# tag pattern, and both behaved identically downstream. Kind comes from the tag. + on: - push: - tags: '[0-9]+.[0-9]+.[0-9]+' + release: + types: [published] + jobs: + resolve-release: + runs-on: ubuntu-latest + outputs: + module: ${{ steps.parse.outputs.module }} + version: ${{ steps.parse.outputs.version }} + kind: ${{ steps.parse.outputs.kind }} + steps: + - name: Parse module, version and release kind from the tag + id: parse + env: + TAG: ${{ github.event.release.tag_name }} + BRANCH: ${{ github.event.release.target_commitish }} + run: | + # Expected: /v[-beta.N] e.g. flowvault/v1.0.0, + # skyvault/v2.1.2, flowvault/v1.0.0-beta.1 + if [[ ! "$TAG" =~ ^[a-z]+/v[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$ ]]; then + echo "::error::Tag '$TAG' is not /v[-beta.N]." \ + "Examples: flowvault/v1.0.0, skyvault/v2.1.2, flowvault/v1.0.0-beta.1" + exit 1 + fi + + PREFIX="${TAG%%/*}" # flowvault/v1.0.0 -> flowvault + VERSION="${TAG#*/}" # flowvault/v1.0.0 -> v1.0.0 + VERSION="${VERSION#v}" # v1.0.0 -> 1.0.0 + + # Tag prefix -> module directory (both match the directory name). + case "$PREFIX" in + flowvault) MODULE="flowvault" ;; + skyvault) MODULE="skyvault" ;; + *) + echo "::error::Unknown module prefix '$PREFIX' in tag '$TAG'" + exit 1 + ;; + esac + + if [[ "$VERSION" == *-beta.* ]]; then KIND="beta"; else KIND="public"; fi + + if [ -z "$BRANCH" ]; then + echo "::error::Release has no target_commitish - cannot determine the release branch." + exit 1 + fi + + echo "Tag '$TAG' -> module='$MODULE' version='$VERSION' kind='$KIND' branch='$BRANCH'" + echo "module=$MODULE" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "kind=$KIND" >> "$GITHUB_OUTPUT" + build-and-deploy: + needs: resolve-release uses: ./.github/workflows/shared-build-and-deploy.yml with: - ref: ${{ github.ref_name }} + ref: ${{ github.event.release.tag_name }} server-id: central profile: maven-central - tag: 'public' + tag: ${{ needs.resolve-release.outputs.kind }} + module: ${{ needs.resolve-release.outputs.module }} + version: ${{ needs.resolve-release.outputs.version }} + release-branch: ${{ github.event.release.target_commitish }} secrets: server-username: ${{ secrets.CENTRAL_PUBLISHER_PORTAL_USERNAME }} server-password: ${{ secrets.CENTRAL_PUBLISHER_PORTAL_PASSWORD }} gpg-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} gpg-passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - skyflow-credentials: ${{ secrets.SKYFLOW_CREDENTIALS }} >> .env - test-expired-token: ${{ secrets.TEST_EXPIRED_TOKEN }} >> .env - test-reusable-token: ${{ secrets.TEST_REUSABLE_TOKEN }} >> .env + skyflow-credentials: ${{ secrets.SKYFLOW_CREDENTIALS }} + test-expired-token: ${{ secrets.TEST_EXPIRED_TOKEN }} + test-reusable-token: ${{ secrets.TEST_REUSABLE_TOKEN }} + pat-actions: ${{ secrets.PAT_ACTIONS }} + test-credentials-file-string: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} diff --git a/.github/workflows/shared-build-and-deploy.yml b/.github/workflows/shared-build-and-deploy.yml index 89e6e8b4..a8ed9dd1 100644 --- a/.github/workflows/shared-build-and-deploy.yml +++ b/.github/workflows/shared-build-and-deploy.yml @@ -21,6 +21,50 @@ on: description: 'Release Tag' required: true type: string + + module: + description: 'Module to build and publish' + required: false + type: string + default: '' + + version: + description: >- + Explicit version to release. Set by tag-triggered (beta/public) + callers, which parse it out of a /v tag - the raw + tag text is not itself a usable Maven version. When empty, the + version is derived as before (see the Bump Version step). + required: false + type: string + default: '' + + release-branch: + description: >- + Branch that receives the version-bump commit, for beta/public + releases. Supplied by the caller from the GitHub Release's + target_commitish - i.e. the branch the human picked in the release + UI. This used to be guessed by matching a branch tip to the tagged + commit, which broke whenever the tip moved on (re-runs always + failed) and silently picked an unrelated branch when several shared + a tip. A tag records only a commit, never a branch, so the branch + has to be supplied rather than inferred. + required: false + type: string + default: '' + + dry-run: + description: >- + Validate the release pipeline WITHOUT publishing anything. + Everything still runs - tag parsing, version resolution, the pom + bump, the full build, tests and GPG signing - but 'mvn verify' + replaces 'mvn deploy' (so the deploy plugin never runs at all, + rather than being asked politely to skip) and the version-bump + commit is not pushed. Publishing to Maven Central is immutable, + so this is the only safe way to exercise the public path. + required: false + type: boolean + default: false + secrets: server-username: required: true @@ -43,6 +87,14 @@ on: test-reusable-token: required: true + # Reusable workflows do NOT inherit caller secrets: anything used below must + # be declared here AND passed by every caller, or it resolves to "" silently. + pat-actions: + required: true + + test-credentials-file-string: + required: true + jobs: publish: runs-on: ubuntu-latest @@ -50,11 +102,9 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - # PAT of the skyflow-service-it admin service account. Persisted by - # checkout and reused for the automated version-bump push below, so - # that push satisfies the branch-protection ruleset's repo-admin - # bypass (github-actions[bot] is not a bypass actor). See SK-2986. - token: ${{ secrets.PAT_ACTIONS }} + # Admin service-account PAT: persisted by checkout and reused for the + # version-bump push, which needs the ruleset's admin bypass. SK-2986. + token: ${{ secrets.pat-actions }} - name: Set up maven or jfrog repository uses: actions/setup-java@v4 @@ -66,19 +116,21 @@ jobs: server-password: SERVER_PASSWORD gpg-private-key: ${{ secrets.gpg-key }} # Value of the GPG private key to import gpg-passphrase: GPG_PASSPHRASE # env variable for GPG private key passphrase - - - name: Resolve Branch for the Tagged Commit - id: resolve-branch - if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} + + - name: Validate release branch input + if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} run: | - TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }}) - BRANCH_NAME=$(git for-each-ref --points-at="$TAG_COMMIT" --format='%(refname:short)' refs/remotes/origin | grep -v '/HEAD$' | sed 's#^origin/##' | head -n 1) - if [ -z "$BRANCH_NAME" ]; then - echo "Error: Could not resolve branch for the tag." + if [ -z "${{ inputs.release-branch }}" ]; then + echo "::error::release-branch is required for ${{ inputs.tag }} releases." exit 1 fi - echo "Resolved Branch Name: $BRANCH_NAME" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_ENV + # The tagged commit must actually be on that branch, otherwise the + # bump would land somewhere the release was never cut from. + if ! git merge-base --is-ancestor HEAD "origin/${{ inputs.release-branch }}"; then + echo "::error::Tagged commit is not an ancestor of origin/${{ inputs.release-branch }}." + exit 1 + fi + echo "Release branch: ${{ inputs.release-branch }}" - name: Get Previous tag id: previoustag @@ -86,13 +138,27 @@ jobs: with: fallback: 1.0.0 + # Version priority: inputs.version (beta/public, parsed from the tag) > + # the module's own pom (internal) > previoustag (unreachable; a safety net). + # Tags are a flat repo-wide namespace with no module awareness, which is why + # internal reads the pom - a tag lookup stamped a v3 version onto every module. - name: Bump Version + id: bump-version run: | - chmod +x ./scripts/bump_version.sh + chmod +x ./scripts/bump_version.sh ./scripts/current_module_version.sh + if [ -n "${{ inputs.version }}" ]; then + BASE_VERSION="${{ inputs.version }}" + elif ${{ inputs.tag == 'internal' }}; then + BASE_VERSION=$(./scripts/current_module_version.sh "${{ inputs.module }}") + else + BASE_VERSION="${{ steps.previoustag.outputs.tag }}" + fi + echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT" + if ${{ inputs.tag == 'internal' }}; then - ./scripts/bump_version.sh "${{ steps.previoustag.outputs.tag }}" "$(git rev-parse --short "$GITHUB_SHA")" + ./scripts/bump_version.sh "$BASE_VERSION" "$(git rev-parse --short "$GITHUB_SHA")" "${{ inputs.module }}" else - ./scripts/bump_version.sh "${{ steps.previoustag.outputs.tag }}" + ./scripts/bump_version.sh "$BASE_VERSION" "" "${{ inputs.module }}" fi - name: Commit changes @@ -101,20 +167,33 @@ jobs: git config user.email ${{ github.actor }}@users.noreply.github.com if [[ "${{ inputs.tag }}" == "beta" || "${{ inputs.tag }}" == "public" ]]; then - git checkout ${{ env.branch_name }} + git checkout ${{ inputs.release-branch }} + fi + + git add ${{ inputs.module }}/pom.xml + + # Nothing staged = pom already at this version (normal if set before + # tagging). That is success; a bare 'git commit' would exit 1 here. + if git diff --cached --quiet; then + echo "::notice::pom already at the target version - nothing to commit" + exit 0 fi - git add pom.xml if [[ "${{ inputs.tag }}" == "internal" ]]; then - # [skip ci]: the version-bump push uses a PAT (see checkout above), and PAT-authored - # pushes DO trigger workflows (unlike GITHUB_TOKEN). Without this marker the push - # re-triggers this same internal-release workflow -> bump -> push -> infinite loop. - git commit -m "[AUTOMATED] Private Release ${{ steps.previoustag.outputs.tag }}-dev-$(git rev-parse --short $GITHUB_SHA) [skip ci]" - git push origin ${{ github.ref_name }} -f + git commit -m "[AUTOMATED] Private Release ${{ steps.bump-version.outputs.base_version }}-dev-$(git rev-parse --short $GITHUB_SHA)" + if [[ "${{ inputs.dry-run }}" == "true" ]]; then + echo "::notice::DRY RUN - not pushing the version-bump commit" + else + git push origin ${{ github.ref_name }} -f + fi fi if [[ "${{ inputs.tag }}" == "beta" || "${{ inputs.tag }}" == "public" ]]; then - git commit -m "[AUTOMATED] Public Release - ${{ steps.previoustag.outputs.tag }}" - git push origin ${{ env.branch_name }} + git commit -m "[AUTOMATED] Public Release - ${{ steps.bump-version.outputs.base_version }}" + if [[ "${{ inputs.dry-run }}" == "true" ]]; then + echo "::notice::DRY RUN - not pushing the version-bump commit" + else + git push origin ${{ inputs.release-branch }} + fi fi - name: Create env @@ -130,12 +209,39 @@ jobs: uses: jsdaniell/create-json@1.1.2 with: name: "credentials.json" - json: ${{ secrets.TEST_CREDENTIALS_FILE_STRING }} + json: ${{ secrets.test-credentials-file-string }} + - name: Distribute test fixtures to modules + 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 skyvault flowvault; do + cp .env "$module/.env" + cp credentials.json "$module/credentials.json" + done + + # Dry run uses 'verify', not 'deploy', so the deploy plugin and the + # central-publishing extension are never invoked at all. + # + # Javadoc is skipped for internal builds only - doclint on JDK 11 fails on + # comment nits. Beta/public must NOT skip it: Sonatype rejects a bundle + # with no -javadoc.jar. - name: Publish package - run: mvn --batch-mode deploy -P ${{ inputs.profile }} + run: | + if [[ "${{ inputs.dry-run }}" == "true" ]]; then + echo "::notice::DRY RUN - running 'verify' instead of 'deploy'; nothing will be published" + if [[ "${{ inputs.tag }}" == "internal" ]]; then + mvn --batch-mode -pl ${{ inputs.module }} -am verify -P jfrog -DskipTests -Dmaven.javadoc.skip=true + else + mvn --batch-mode -pl ${{ inputs.module }} -am verify -P ${{ inputs.profile }} + fi + elif [[ "${{ inputs.tag }}" == "internal" ]]; then + mvn --batch-mode -pl ${{ inputs.module }} -am deploy -P jfrog -DskipTests -Dmaven.javadoc.skip=true + elif [[ "${{ inputs.tag }}" == "beta" || "${{ inputs.tag }}" == "public" ]]; then + mvn --batch-mode -pl ${{ inputs.module }} -am deploy -P ${{ inputs.profile }} + fi env: SERVER_USERNAME: ${{ secrets.server-username }} SERVER_PASSWORD: ${{ secrets.server-password }} - GPG_PASSPHRASE: ${{ secrets.gpg-passphrase }} + GPG_PASSPHRASE: ${{ secrets.gpg-passphrase }} \ No newline at end of file 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/README.md b/README.md index 96b1446c..a5bce548 100644 --- a/README.md +++ b/README.md @@ -1,3149 +1,37 @@ # Skyflow Java -> **This is the current, recommended version of the Skyflow SDK.** V2.1.0 brings flexible auth, multi-vault support, builder patterns, native data types, and rich error diagnostics. -> -> Migrating from v1? See the **[Migration Guide](docs/migrate_to_v2.md)** for step-by-step instructions. V1 is in maintenance mode and will reach End of Life on October 31, 2026. - -The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend. +This repository hosts Skyflow's Java SDKs for integrating Skyflow into a Java backend. It's a single Maven reactor with more than one published artifact — pick the package that matches what you need below. [![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) -[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-java.svg)](https://mvnrepository.com/artifact/com.skyflow/skyflow-java) +[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-java.svg)](https://github.com/skyflowapi/skyflow-java/releases) [![License](https://img.shields.io/github/license/skyflowapi/skyflow-java)](https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE) -# Table of Contents - -- [Table of Contents](#table-of-contents) -- [Overview](#overview) -- [Install](#install) - - [Requirements](#requirements) - - [Configuration](#configuration) - - [Gradle users](#gradle-users) - - [Maven users](#maven-users) -- [API Reference](docs/api_reference.md) -- [Migration from v1 to v2](docs/migrate_to_v2.md) -- [Quickstart](#quickstart) - - [Authenticate](#authenticate) - - [Initialize the client](#initialize-the-client) - - [Insert data into the vault](#insert-data-into-the-vault) -- [Vault](#vault) - - [VaultController](#vaultcontroller) - - [Insert data into the vault](#insert-data-into-the-vault-1) - - [Detokenize](#detokenize) - - [DetokenizeRecordResponse](#detokenizerecordresponse) - - [Tokenize](#tokenize) - - [Get](#get) - - [Get by skyflow IDS](#get-by-skyflow-ids) - - [Get tokens](#get-tokens) - - [Get by column name and column values](#get-by-column-name-and-column-values) - - [Redaction types](#redaction-types) - - [Update](#update) - - [Delete](#delete) - - [Query](#query) - - [Upload File](#upload-file) - -- [Detect](#detect) - - [Deidentify Text](#deidentify-text) - - [Reidentify Text](#reidentify-text) - - [Deidentify File](#deidentify-file) - - [Get Run](#get-run) - - [Detect response types](#detect-response-types) - - [Detect enums](#detect-enums) -- [Connections](#connections) - - [ConnectionController](#connectioncontroller) - - [Invoke a connection](#invoke-a-connection) -- [Client Management](#client-management) -- [Authenticate with bearer tokens](#authenticate-with-bearer-tokens) - - [Generate a bearer token](#generate-a-bearer-token) - - [Generate bearer tokens with context](#generate-bearer-tokens-with-context) - - [Generate scoped bearer tokens](#generate-scoped-bearer-tokens) - - [Generate signed data tokens](#generate-signed-data-tokens) - - [Bearer token expiry edge case](#bearer-token-expiry-edge-case) -- [Error Handling](#error-handling) - - [Catching SkyflowException](#catching-skyflowexception) - - [SkyflowException properties](#skyflowexception-properties) -- [Logging](#logging) -- [Reporting a Vulnerability](#reporting-a-vulnerability) - -# Overview - -- Authenticate using a Skyflow service account and generate bearer tokens for secure access. -- Perform Vault API operations such as inserting, retrieving, and tokenizing sensitive data with ease. -- Invoke connections to third-party APIs without directly handling sensitive data, ensuring compliance and data protection. - -> [!TIP] -> Looking for the full list of request builder methods, response getters, enums, helper class APIs, and service-account utilities? See the **[API Reference](docs/api_reference.md)**. - -# Install - -## Requirements - -- Java 8 and above (tested with Java 8) - -## Configuration - ---- - -### Gradle users - -Add this dependency to your project's `build.gradle` file: - -``` -implementation 'com.skyflow:skyflow-java:2.0.0' -``` - -### Maven users - -Add this dependency to your project's `pom.xml` file: - -```xml - - com.skyflow - skyflow-java - 2.0.0 - -``` - ---- - -# Migrate from v1 to v2 - -Upgrading from v1? See the dedicated migration guide: **[docs/migrate_to_v2.md](docs/migrate_to_v2.md)** - -# Quickstart - -Get started quickly with the essential steps: authenticate, initialize the client, and perform a basic vault operation. This section provides a minimal setup to help you integrate the SDK efficiently. - -### Authenticate - -You can use an API key to authenticate and authorize requests to an API. For authenticating via bearer tokens and different supported bearer token types, refer to the [Authenticate with bearer tokens](#authenticate-with-bearer-tokens) section. - -```java -// create a new credentials object -Credentials credentials = new Credentials(); -credentials.setApiKey(""); // add your API key in credentials -``` - -### Initialize the client - -To get started, you must first initialize the skyflow client. While initializing the skyflow client, you can specify different types of credentials. - -1. **API keys** - A unique identifier used to authenticate and authorize requests to an API. - -2. **Bearer tokens** - A temporary access token used to authenticate API requests, typically included in the Authorization header. - -3. **Service account credentials file path** - The file path pointing to a JSON file containing credentials for a service account, used for secure API access. - -4. **Service account credentials string (JSON formatted)** - A JSON-formatted string containing service account credentials, often used as an alternative to a file for programmatic authentication. - -Note: Only one type of credential can be used at a time. If multiple credentials are provided, the last one added will take precedence. - -```java -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; - -/** - * Example program to initialize the Skyflow client with various configurations. - * The Skyflow client facilitates secure interactions with the Skyflow vault, - * such as securely managing sensitive data. - */ -public class InitSkyflowClient { - public static void main(String[] args) throws SkyflowException { - // Step 1: Define the primary credentials for authentication. - // Note: Only one type of credential can be used at a time. You can choose between: - // - API key - // - Bearer token - // - A credentials string (JSON-formatted) - // - A file path to a credentials file. - - // Initialize primary credentials using a Bearer token for authentication. - Credentials primaryCredentials = new Credentials(); - primaryCredentials.setToken(""); // Replace with your actual authentication token. - - // Step 2: Configure the primary vault details. - // VaultConfig stores all necessary details to connect to a specific Skyflow vault. - VaultConfig primaryConfig = new VaultConfig(); - primaryConfig.setVaultId(""); // Replace with your primary vault's ID. - primaryConfig.setClusterId(""); // Replace with the cluster ID (part of the vault URL, e.g., https://{clusterId}.vault.skyflowapis.com). - primaryConfig.setEnv(Env.PROD); // Set the environment (PROD, SANDBOX, STAGE, DEV). - primaryConfig.setCredentials(primaryCredentials); // Attach the primary credentials to this vault configuration. - - // Step 3: Create credentials as a JSON object (if a Bearer Token is not provided). - // Demonstrates an alternate approach to authenticate with Skyflow using a credentials object. - JsonObject credentialsObject = new JsonObject(); - credentialsObject.addProperty("clientId", ""); // Replace with your Client ID. - credentialsObject.addProperty("clientName", ""); // Replace with your Client Name. - credentialsObject.addProperty("tokenUri", ""); // Replace with the Token URI. - credentialsObject.addProperty("keyId", ""); // Replace with your Key ID. - credentialsObject.addProperty("privateKey", ""); // Replace with your Private Key. - - // Step 4: Convert the JSON object to a string and use it as credentials. - // This approach allows the use of dynamically generated or pre-configured credentials. - Credentials skyflowCredentials = new Credentials(); - skyflowCredentials.setCredentialsString(credentialsObject.toString()); // Converts JSON object to string for use as credentials. - - // Step 5: Define secondary credentials (API key-based authentication as an example). - // Demonstrates a different type of authentication mechanism for Skyflow vaults. - Credentials secondaryCredentials = new Credentials(); - secondaryCredentials.setApiKey(""); // Replace with your API Key for authentication. - - // Step 6: Configure the secondary vault details. - // A secondary vault configuration can be used for operations involving multiple vaults. - VaultConfig secondaryConfig = new VaultConfig(); - secondaryConfig.setVaultId(""); // Replace with your secondary vault's ID. - secondaryConfig.setClusterId(""); // Replace with the corresponding cluster ID. - secondaryConfig.setEnv(Env.SANDBOX); // Set the environment for this vault. - secondaryConfig.setCredentials(secondaryCredentials); // Attach the secondary credentials to this configuration. - - // Step 7: Define tertiary credentials using a path to a credentials JSON file. - // This method demonstrates an alternative authentication method. - Credentials tertiaryCredentials = new Credentials(); - tertiaryCredentials.setPath(""); // Replace with the path to your credentials file. - - // Step 8: Configure the tertiary vault details. - VaultConfig tertiaryConfig = new VaultConfig(); - tertiaryConfig.setVaultId(""); // Replace with the tertiary vault ID. - tertiaryConfig.setClusterId(""); // Replace with the corresponding cluster ID. - tertiaryConfig.setEnv(Env.STAGE); // Set the environment for this vault. - tertiaryConfig.setCredentials(tertiaryCredentials); // Attach the tertiary credentials. - - // Step 9: Build and initialize the Skyflow client. - // Skyflow client is configured with multiple vaults and credentials. - Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.INFO) // Set log level for debugging or monitoring purposes. - .addVaultConfig(primaryConfig) // Add the primary vault configuration. - .addVaultConfig(secondaryConfig) // Add the secondary vault configuration. - .addVaultConfig(tertiaryConfig) // Add the tertiary vault configuration. - .addSkyflowCredentials(skyflowCredentials) // Add JSON-formatted credentials if applicable. - .build(); - - // The Skyflow client is now fully initialized. - // Use the `skyflowClient` object to perform secure operations such as: - // - Inserting data - // - Retrieving data - // - Deleting data - // within the configured Skyflow vaults. - } -} -``` - -Notes: - -- If both Skyflow common credentials and individual credentials at the configuration level are specified, the individual credentials at the configuration level will take precedence. -- If neither Skyflow common credentials nor individual configuration-level credentials are provided, the SDK attempts to retrieve credentials from the `SKYFLOW_CREDENTIALS` environment variable. -- All Vault operations require a client instance. -- `Credentials.setContext()` accepts either a `String` or a `Map` for context-aware authorization. See [Generate bearer tokens with context](#generate-bearer-tokens-with-context) for full usage. - -### Insert data into the vault - -To insert data into your vault, use the `insert` method. The `InsertRequest` class creates an insert request, which includes the values to be inserted as a list of records. Below is a simple example to get started. For advanced options, check out [Insert data into the vault](#insert-data-into-the-vault-1) section. - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * This example demonstrates how to insert sensitive data (e.g., card information) into a Skyflow vault using the Skyflow client. - * - * 1. Initializes the Skyflow client. - * 2. Prepares a record with sensitive data (e.g., card number and cardholder name). - * 3. Creates an insert request for inserting the data into the Skyflow vault. - * 4. Prints the response of the insert operation. - */ -public class InsertExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize data to be inserted into the Skyflow vault - ArrayList> insertData = new ArrayList<>(); - - // Create a HashMap for a single record with card number and cardholder name as fields - HashMap insertRecord = new HashMap<>(); - insertRecord.put("card_number", "4111111111111111"); // Replace with actual card number (sensitive data) - insertRecord.put("cardholder_name", "john doe"); // Replace with actual cardholder name (sensitive data) - - // Add the created record to the list of data to be inserted - insertData.add(insertRecord); - - // Step 2: Build the InsertRequest object with the table name and data to insert - InsertRequest insertRequest = InsertRequest.builder() - .table("table1") // Specify the table in the vault where the data will be inserted - .values(insertData) // Attach the data (records) to be inserted - .returnTokens(true) // Specify if tokens should be returned upon successful insertion - .build(); // Build the insert request object - - // Step 3: Perform the insert operation using the Skyflow client - InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); - // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 4: Print the response from the insert operation - System.out.println(insertResponse); - } catch (SkyflowException e) { - // Step 5: Handle any exceptions that may occur during the insert operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the stack trace for debugging purposes - } - } -} -``` - -Skyflow returns tokens for the record that was just inserted. - -```json -{ - "insertedFields": [ - { - "card_number": "5484-7829-1702-9110", - "requestIndex": "0", - "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" - } - ], - "errors": [] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -# Vault - -The [Vault](https://github.com/skyflowapi/skyflow-java/tree/main/samples/src/main/java/com/example/vault) module performs operations on the vault, including inserting records, detokenizing tokens, and retrieving tokens associated with a `skyflow_id`. - -## VaultController - -`VaultController` is the class returned by `skyflowClient.vault()` and `skyflowClient.vault(vaultId)`. All vault operations are called on this object. - -```java -// Uses the default (first configured) vault -VaultController vault = skyflowClient.vault(); - -// Uses a specific vault by ID -VaultController vault = skyflowClient.vault(""); -``` - -**Methods:** - -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `insert(InsertRequest)` | [`InsertRequest`](docs/api_reference.md#insertrequest) | [`InsertResponse`](docs/api_reference.md#insertresponse) | Insert one or more records | -| `detokenize(DetokenizeRequest)` | [`DetokenizeRequest`](docs/api_reference.md#detokenizerequest) | [`DetokenizeResponse`](docs/api_reference.md#detokenizeresponse) | Detokenize tokens to their original values | -| `tokenize(TokenizeRequest)` | [`TokenizeRequest`](docs/api_reference.md#tokenizerequest) | [`TokenizeResponse`](docs/api_reference.md#tokenizeresponse) | Tokenize sensitive values | -| `get(GetRequest)` | [`GetRequest`](docs/api_reference.md#getrequest) | [`GetResponse`](docs/api_reference.md#getresponse) | Retrieve records by Skyflow ID or column value | -| `update(UpdateRequest)` | [`UpdateRequest`](docs/api_reference.md#updaterequest) | [`UpdateResponse`](docs/api_reference.md#updateresponse) | Update a record by Skyflow ID | -| `delete(DeleteRequest)` | [`DeleteRequest`](docs/api_reference.md#deleterequest) | [`DeleteResponse`](docs/api_reference.md#deleteresponse) | Delete records by Skyflow ID | -| `query(QueryRequest)` | [`QueryRequest`](docs/api_reference.md#queryrequest) | [`QueryResponse`](docs/api_reference.md#queryresponse) | Execute a SQL query | -| `uploadFile(FileUploadRequest)` | [`FileUploadRequest`](docs/api_reference.md#fileuploadrequest) | [`FileUploadResponse`](docs/api_reference.md#fileuploadresponse) | Upload a file to a vault column | - -All methods throw `SkyflowException` on error. - -## Insert data into the vault - -Apart from using the `insert` method to insert data into your vault covered in [Quickstart](#quickstart), you can also specify options in [`InsertRequest`](docs/api_reference.md#insertrequest), such as returning tokenized data, upserting records, or continuing the operation in case of errors. Returns an [`InsertResponse`](docs/api_reference.md#insertresponse). - -### Construct an insert request - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * Example program to demonstrate inserting data into a Skyflow vault, along with corresponding InsertRequest schema. - * - */ -public class InsertSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Prepare the data to be inserted into the Skyflow vault - ArrayList> insertData = new ArrayList<>(); - - // Create the first record with field names and their respective values - HashMap insertRecord1 = new HashMap<>(); - insertRecord1.put("", ""); // Replace with actual field name and value - insertRecord1.put("", ""); // Replace with actual field name and value - - // Create the second record with field names and their respective values - HashMap insertRecord2 = new HashMap<>(); - insertRecord2.put("", ""); // Replace with actual field name and value - insertRecord2.put("", ""); // Replace with actual field name and value - - // Add the records to the list of data to be inserted - insertData.add(insertRecord1); - insertData.add(insertRecord2); - - // Step 2: Build an InsertRequest object with the table name and the data to insert - InsertRequest insertRequest = InsertRequest.builder() - .table("") // Replace with the actual table name in your Skyflow vault - .values(insertData) // Attach the data to be inserted - .build(); - - // Step 3: Use the Skyflow client to perform the insert operation - InsertResponse insertResponse = skyflowClient.vault("").insert(insertRequest); - // Replace with your actual vault ID - - // Print the response from the insert operation - System.out.println("Insert Response: " + insertResponse); - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the insert operation - System.out.println("Error occurred while inserting data: "); - e.printStackTrace(); // Print the stack trace for debugging - } - } -} -``` - -### Insert call [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/InsertExample.java) with `continueOnError` option - -The `continueOnError` flag is a boolean that determines whether insert operation should proceed despite encountering partial errors. Set to `true` to allow the process to continue even if some errors occur. - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * This example demonstrates how to insert multiple records into a Skyflow vault using the Skyflow client. - * - * 1. Initializes the Skyflow client. - * 2. Prepares multiple records with sensitive data (e.g., card number and cardholder name). - * 3. Creates an insert request with the records to insert into the Skyflow vault. - * 4. Specifies options to continue on error and return tokens. - * 5. Prints the response of the insert operation. - */ -public class InsertExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list to hold the data records to be inserted into the vault - ArrayList> insertData = new ArrayList<>(); - - // Step 2: Create the first record with card number and cardholder name - HashMap insertRecord1 = new HashMap<>(); - insertRecord1.put("card_number", "4111111111111111"); // Replace with actual card number (sensitive data) - insertRecord1.put("cardholder_name", "john doe"); // Replace with actual cardholder name (sensitive data) - - // Step 3: Create the second record with card number and cardholder name - HashMap insertRecord2 = new HashMap<>(); - insertRecord2.put("card_number", "4111111111111111"); // Ensure field name matches ("card_number") - insertRecord2.put("cardholder_name", "jane doe"); // Replace with actual cardholder name (sensitive data) - - // Step 4: Add the records to the insertData list - insertData.add(insertRecord1); - insertData.add(insertRecord2); - - // Step 5: Build the InsertRequest object with the data records to insert - InsertRequest insertRequest = InsertRequest.builder() - .table("table1") // Specify the table in the vault where data will be inserted - .values(insertData) // Attach the data records to be inserted - .returnTokens(true) // Specify if tokens should be returned upon successful insertion - .continueOnError(true) // Specify to continue inserting records even if an error occurs for some records - .build(); // Build the insert request object - - // Step 6: Perform the insert operation using the Skyflow client - InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); - // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 7: Print the response from the insert operation - System.out.println(insertResponse); - } catch (SkyflowException e) { - // Step 8: Handle any exceptions that may occur during the insert operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the stack trace for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "insertedFields": [ - { - "card_number": "5484-7829-1702-9110", - "requestIndex": "0", - "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" - } - ], - "errors": [ - { - "requestIndex": "1", - "error": "Insert failed. Column card_number is invalid. Specify a valid column." - } - ] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -### Insert call example with `upsert` option - -An upsert operation checks for a record based on a unique column's value. If a match exists, the record is updated; otherwise, a new record is inserted. - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.InsertRequest; -import com.skyflow.vault.data.InsertResponse; - -import java.util.ArrayList; -import java.util.HashMap; - -/** - * This example demonstrates how to insert or upsert a record into a Skyflow vault using the Skyflow client, with the option to return tokens. - * - * 1. Initializes the Skyflow client. - * 2. Prepares a record to insert or upsert (e.g., cardholder name). - * 3. Creates an insert request with the data to be inserted or upserted into the Skyflow vault. - * 4. Specifies the field (cardholder_name) for upsert operations. - * 5. Prints the response of the insert or upsert operation. - */ -public class UpsertExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list to hold the data records for the insert/upsert operation - ArrayList> upsertData = new ArrayList<>(); - - // Step 2: Create a record with the field 'cardholder_name' to insert or upsert - HashMap upsertRecord = new HashMap<>(); - upsertRecord.put("cardholder_name", "jane doe"); // Replace with the actual cardholder name - - // Step 3: Add the record to the upsertData list - upsertData.add(upsertRecord); - - // Step 4: Build the InsertRequest object with the upsertData - InsertRequest insertRequest = InsertRequest.builder() - .table("table1") // Specify the table in the vault where data will be inserted/upserted - .values(upsertData) // Attach the data records to be inserted/upserted - .returnTokens(true) // Specify if tokens should be returned upon successful operation - .upsert("cardholder_name") // Specify the field to be used for upsert operations (e.g., cardholder_name) - .build(); // Build the insert request object - - // Step 5: Perform the insert/upsert operation using the Skyflow client - InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); - // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 6: Print the response from the insert/upsert operation - System.out.println(insertResponse); - } catch (SkyflowException e) { - // Step 7: Handle any exceptions that may occur during the insert/upsert operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the stack trace for debugging purposes - } - } -} -``` - -Skyflow returns tokens, with `upsert` support, for the record you just inserted. - -```json -{ - "insertedFields": [ - { - "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "cardholder_name": "73ce45ce-20fd-490e-9310-c1d4f603ee83" - } - ], - "errors": [] -} -``` - -## Detokenize - -To retrieve tokens from your vault, use the `detokenize` method. [`DetokenizeRequest`](docs/api_reference.md#detokenizerequest) requires a list of detokenization data as input. Returns a [`DetokenizeResponse`](docs/api_reference.md#detokenizeresponse). - -### Construct a detokenize request - -Each entry in the detokenize list is a [`DetokenizeData`](docs/api_reference.md#detokenizedata) object pairing a token with its desired redaction type. See the [API Reference](docs/api_reference.md#detokenizerequest) for all `DetokenizeRequest` builder options. - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.DetokenizeRequest; -import com.skyflow.vault.tokens.DetokenizeResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to detokenize sensitive data from tokens stored in a Skyflow vault, along with corresponding DetokenizeRequest schema. - * - */ -public class DetokenizeSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of tokens to be detokenized (replace with actual tokens) - ArrayList detokenizeData1 = new ArrayList<>(); - DetokenizeData detokenizeDataRecord1 = new DetokenizeData("", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction - DetokenizeData detokenizeDataRecord2 = new DetokenizeData("", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction - detokenizeData1.add(detokenizeDataRecord1); - detokenizeData1.add(detokenizeDataRecord2); - - // Step 2: Create the DetokenizeRequest object with the tokens and redaction type - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types - .continueOnError(true) // Continue even if one token cannot be detokenized - .build(); // Build the detokenization request - - // Step 3: Call the Skyflow vault to detokenize the provided tokens - DetokenizeResponse detokenizeResponse = skyflowClient.vault("").detokenize(detokenizeRequest); - // Replace with your actual Skyflow vault ID - - // Step 4: Print the detokenization response, which contains the detokenized data - System.out.println(detokenizeResponse); - } catch (SkyflowException e) { - // Step 5: Handle any errors that occur during the detokenization process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Notes: - -- `redactionType` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). -- `continueOnError` defaults to `true`. - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/DetokenizeExample.java) of a detokenize call: - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.DetokenizeRequest; -import com.skyflow.vault.tokens.DetokenizeResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to detokenize sensitive data from tokens stored in a Skyflow vault. - * - * 1. Initializes the Skyflow client. - * 2. Creates a list of tokens (e.g., credit card tokens) that represent the sensitive data. - * 3. Builds a detokenization request using the provided tokens and specifies how the redacted data should be returned. - * 4. Calls the Skyflow vault to detokenize the tokens and retrieves the detokenized data. - * 5. Prints the detokenization response, which contains the detokenized values or errors. - */ -public class DetokenizeExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) - ArrayList detokenizeData1 = new ArrayList<>(); - DetokenizeData detokenizeDataRecord1 = new DetokenizeData("9738-1683-0486-1480", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction - DetokenizeData detokenizeDataRecord2 = new DetokenizeData("6184-6357-8409-6668", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction - detokenizeData1.add(detokenizeDataRecord1); - detokenizeData1.add(detokenizeDataRecord2); - - // Step 2: Create the DetokenizeRequest object with the tokens and redaction type - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types - .continueOnError(true) // Continue even if one token cannot be detokenized - .build(); // Build the detokenization request - - // Step 3: Call the Skyflow vault to detokenize the provided tokens - DetokenizeResponse detokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").detokenize(detokenizeRequest); - // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 4: Print the detokenization response, which contains the detokenized data - System.out.println(detokenizeResponse); - } catch (SkyflowException e) { - // Step 5: Handle any errors that occur during the detokenization process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "detokenizedFields": [{ - "token": "9738-1683-0486-1480", - "value": "4111111111111115", - "type": "STRING", - }, { - "token": "6184-6357-8409-6668", - "value": "4111111111111119", - "type": "STRING", - }], - "errors": [] -} - -``` - -### DetokenizeRecordResponse - -`DetokenizeResponse.getDetokenizedFields()` and `DetokenizeResponse.getErrors()` each return a `List`. Use this class to read individual token results: - -```java -DetokenizeResponse detokenizeResponse = skyflowClient.vault("").detokenize(detokenizeRequest); - -for (DetokenizeRecordResponse record : detokenizeResponse.getDetokenizedFields()) { - System.out.println("Token : " + record.getToken()); - System.out.println("Value : " + record.getValue()); - System.out.println("Type : " + record.getType()); - System.out.println("ReqID : " + record.getRequestId()); -} - -for (DetokenizeRecordResponse err : detokenizeResponse.getErrors()) { - System.out.println("Failed token : " + err.getToken()); - System.out.println("Error : " + err.getError()); -} -``` - -See [`DetokenizeRecordResponse`](docs/api_reference.md#detokenizerecordresponse) in the API Reference for the full attribute list. - -### An example of a detokenize call with `continueOnError` option: - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.DetokenizeRequest; -import com.skyflow.vault.tokens.DetokenizeResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to detokenize sensitive data (e.g., credit card numbers) from tokens in a Skyflow vault. - * - * 1. Initializes the Skyflow client. - * 2. Creates a list of tokens (e.g., credit card tokens) to be detokenized. - * 3. Builds a detokenization request with the tokens and specifies the redaction type for the detokenized data. - * 4. Calls the Skyflow vault to detokenize the tokens and retrieves the detokenized data. - * 5. Prints the detokenization response, which includes the detokenized values or errors. - */ -public class DetokenizeExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) - // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) - ArrayList detokenizeData1 = new ArrayList<>(); - DetokenizeData detokenizeDataRecord1 = new DetokenizeData("9738-1683-0486-1480", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction - DetokenizeData detokenizeDataRecord2 = new DetokenizeData("6184-6357-8409-6668", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction - DetokenizeData detokenizeDataRecord2 = new DetokenizeData("4914-9088-2814-384", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction - - detokenizeData1.add(detokenizeDataRecord1); - detokenizeData1.add(detokenizeDataRecord2); - - // Step 2: Create the DetokenizeRequest object with the tokens and redaction type - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types - .continueOnError(true) // Continue even if one token cannot be detokenized - .build(); // Build the detokenization request - - // Step 3: Call the Skyflow vault to detokenize the provided tokens - DetokenizeResponse detokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").detokenize(detokenizeRequest); - // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 4: Print the detokenization response, which contains the detokenized data or errors - System.out.println(detokenizeResponse); - } catch (SkyflowException e) { - // Step 5: Handle any errors that occur during the detokenization process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "detokenizedFields": [{ - "token": "9738-1683-0486-1480", - "value": "4111111111111115", - "type": "STRING", - }, { - "token": "6184-6357-8409-6668", - "value": "4111111111111119", - "type": "STRING", - }], - "errors": [{ - "token": "4914-9088-2814-384", - "error": "Token Not Found", - }] -} -``` - -## Tokenize - -Tokenization replaces sensitive data with unique identifier tokens. This approach protects sensitive information by securely storing the original data while allowing the use of tokens within your application. - -To tokenize data, use the `tokenize` method. [`TokenizeRequest`](docs/api_reference.md#tokenizerequest) accepts a list of [`ColumnValue`](docs/api_reference.md#columnvalue) objects. Returns a [`TokenizeResponse`](docs/api_reference.md#tokenizeresponse). - -### Construct a tokenize request - -Each entry in the tokenize list is a [`ColumnValue`](docs/api_reference.md#columnvalue) object pairing a value with its column group. See the [API Reference](docs/api_reference.md#tokenizerequest) for all `TokenizeRequest` builder options. - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.ColumnValue; -import com.skyflow.vault.tokens.TokenizeRequest; -import com.skyflow.vault.tokens.TokenizeResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to tokenize sensitive data (e.g., credit card information) using the Skyflow client, along with corresponding TokenizeRequest schema. - * - */ -public class TokenizeSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of column values to be tokenized (replace with actual sensitive data) - ArrayList columnValues = new ArrayList<>(); - - // Step 2: Create column values for each sensitive data field (e.g., card number and cardholder name) - ColumnValue columnValue1 = ColumnValue.builder().value("").columnGroup("").build(); // Replace and with actual data - ColumnValue columnValue2 = ColumnValue.builder().value("").columnGroup("").build(); // Replace and with actual data - - // Add the created column values to the list - columnValues.add(columnValue1); - columnValues.add(columnValue2); - - // Step 3: Build the TokenizeRequest with the column values - TokenizeRequest tokenizeRequest = TokenizeRequest.builder().values(columnValues).build(); - - // Step 4: Call the Skyflow vault to tokenize the sensitive data - TokenizeResponse tokenizeResponse = skyflowClient.vault("").tokenize(tokenizeRequest); - // Replace with your actual Skyflow vault ID - - // Step 5: Print the tokenization response, which contains the generated tokens or errors - System.out.println(tokenizeResponse); - } catch (SkyflowException e) { - // Step 6: Handle any errors that occur during the tokenization process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/TokenizeExample.java) of Tokenize call: - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.tokens.ColumnValue; -import com.skyflow.vault.tokens.TokenizeRequest; -import com.skyflow.vault.tokens.TokenizeResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to tokenize sensitive data (e.g., credit card information) using the Skyflow client. - * - * 1. Initializes the Skyflow client. - * 2. Creates a column value for sensitive data (e.g., credit card number). - * 3. Builds a tokenize request with the column value to be tokenized. - * 4. Sends the request to the Skyflow vault for tokenization. - * 5. Prints the tokenization response, which includes the token or errors. - */ -public class TokenizeExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of column values to be tokenized (replace with actual sensitive data) - ArrayList columnValues = new ArrayList<>(); - - // Step 2: Create a column value for the sensitive data (e.g., card number with its column group) - ColumnValue columnValue = ColumnValue.builder() - .value("4111111111111111") // Replace with the actual sensitive data (e.g., card number) - .columnGroup("card_number_cg") // Replace with the actual column group name - .build(); - - // Add the created column value to the list - columnValues.add(columnValue); - - // Step 3: Build the TokenizeRequest with the column value - TokenizeRequest tokenizeRequest = TokenizeRequest.builder().values(columnValues).build(); - - // Step 4: Call the Skyflow vault to tokenize the sensitive data - TokenizeResponse tokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").tokenize(tokenizeRequest); - // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID - - // Step 5: Print the tokenization response, which contains the generated token or any errors - System.out.println(tokenizeResponse); - } catch (SkyflowException e) { - // Step 6: Handle any errors that occur during the tokenization process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "tokens": [5479-4229-4622-1393] -} -``` - -## Get - -To retrieve data using Skyflow IDs or unique column values, use the `get` method. [`GetRequest`](docs/api_reference.md#getrequest) accepts parameters such as table name, redaction type, Skyflow IDs, column names, and column values. `ids` and `columnName`/`columnValues` are mutually exclusive. Returns a [`GetResponse`](docs/api_reference.md#getresponse). - -### Construct a get request - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.data.GetResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to retrieve data from the Skyflow vault using different methods, along with corresponding GetRequest schema. - * - */ -public class GetSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of Skyflow IDs to retrieve records (replace with actual Skyflow IDs) - ArrayList ids = new ArrayList<>(); - ids.add(""); // Replace with actual Skyflow ID - ids.add(""); // Replace with actual Skyflow ID - - // Step 2: Create a GetRequest to retrieve records by Skyflow ID without returning tokens - GetRequest getByIdRequest = GetRequest.builder() - .ids(ids) - .table("") // Replace with the actual table name - .returnTokens(false) // Set to false to avoid returning tokens - .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text - .build(); - - // Send the request to the Skyflow vault and retrieve the records - GetResponse getByIdResponse = skyflowClient.vault("").get(getByIdRequest); // Replace with actual Vault ID - System.out.println(getByIdResponse); - - // Step 3: Create another GetRequest to retrieve records by Skyflow ID with tokenized values - GetRequest getTokensRequest = GetRequest.builder() - .ids(ids) - .table("") // Replace with the actual table name - .returnTokens(true) // Set to true to return tokenized values - .build(); - - // Send the request to the Skyflow vault and retrieve the tokenized records - GetResponse getTokensResponse = skyflowClient.vault("").get(getTokensRequest); // Replace with actual Vault ID - System.out.println(getTokensResponse); - - // Step 4: Create a GetRequest to retrieve records based on specific column values - ArrayList columnValues = new ArrayList<>(); - columnValues.add(""); // Replace with the actual column value - columnValues.add(""); // Replace with the actual column value - - GetRequest getByColumnRequest = GetRequest.builder() - .table("") // Replace with the actual table name - .columnName("") // Replace with the column name - .columnValues(columnValues) // Add the list of column values to filter by - .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text - .build(); - - // Send the request to the Skyflow vault and retrieve the records filtered by column values - GetResponse getByColumnResponse = skyflowClient.vault("").get(getByColumnRequest); // Replace with actual Vault ID - System.out.println(getByColumnResponse); - } catch (SkyflowException e) { - // Step 5: Handle any errors that occur during the retrieval process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -### Get by skyflow IDs - -Retrieve specific records using `skyflow_ids`. Ideal for fetching exact records when IDs are known. - -#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/GetExample.java) of a get call to retrieve data using Redaction type: - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.data.GetResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to retrieve data from the Skyflow vault using a list of Skyflow IDs. - * - * 1. Initializes the Skyflow client with a given vault ID. - * 2. Creates a request to retrieve records based on Skyflow IDs. - * 3. Specifies that the response should not return tokens. - * 4. Uses plain text redaction type for the retrieved records. - * 5. Prints the response to display the retrieved records. - */ -public class GetExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of Skyflow IDs (replace with actual Skyflow IDs) - ArrayList ids = new ArrayList<>(); - ids.add("a581d205-1969-4350-acbe-a2a13eb871a6"); // Replace with actual Skyflow ID - ids.add("5ff887c3-b334-4294-9acc-70e78ae5164a"); // Replace with actual Skyflow ID - - // Step 2: Create a GetRequest to retrieve records based on Skyflow IDs - // The request specifies: - // - `ids`: The list of Skyflow IDs to retrieve - // - `table`: The table from which the records will be retrieved - // - `returnTokens`: Set to false, meaning tokens will not be returned in the response - // - `redactionType`: Set to PLAIN_TEXT, meaning the retrieved records will have data redacted as plain text - GetRequest getByIdRequest = GetRequest.builder() - .ids(ids) - .table("table1") // Replace with the actual table name - .returnTokens(false) // Set to false to avoid returning tokens - .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text - .build(); - - // Step 3: Send the request to the Skyflow vault and retrieve the records - GetResponse getByIdResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getByIdRequest); // Replace with actual Vault ID - System.out.println(getByIdResponse); // Print the response to the console - - } catch (SkyflowException e) { - // Step 4: Handle any errors that occur during the data retrieval process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "data": [ - { - "card_number": "4555555555555553", - "email": "john.doe@gmail.com", - "name": "john doe", - "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" - }, - { - "card_number": "4555555555555559", - "email": "jane.doe@gmail.com", - "name": "jane doe", - "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" - } - ], - "errors": [] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -### Get tokens - -Return tokens for records. Ideal for securely processing sensitive data while maintaining data privacy. - -#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/getExample.java) of get call to retrieve tokens using Skyflow IDs: - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.data.GetResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to retrieve data from the Skyflow vault and return tokens along with the records. - * - * 1. Initializes the Skyflow client with a given vault ID. - * 2. Creates a request to retrieve records based on Skyflow IDs and ensures tokens are returned. - * 3. Prints the response to display the retrieved records along with the tokens. - */ -public class GetExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of Skyflow IDs (replace with actual Skyflow IDs) - ArrayList ids = new ArrayList<>(); - ids.add("a581d205-1969-4350-acbe-a2a13eb871a6"); // Replace with actual Skyflow ID - ids.add("5ff887c3-b334-4294-9acc-70e78ae5164a"); // Replace with actual Skyflow ID - - // Step 2: Create a GetRequest to retrieve records based on Skyflow IDs - // The request specifies: - // - `ids`: The list of Skyflow IDs to retrieve - // - `table`: The table from which the records will be retrieved - // - `returnTokens`: Set to true, meaning tokens will be included in the response - GetRequest getTokensRequest = GetRequest.builder() - .ids(ids) - .table("table1") // Replace with the actual table name - .returnTokens(true) // Set to true to include tokens in the response - .build(); - - // Step 3: Send the request to the Skyflow vault and retrieve the records with tokens - GetResponse getTokensResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getTokensRequest); // Replace with actual Vault ID - System.out.println(getTokensResponse); // Print the response to the console - - } catch (SkyflowException e) { - // Step 4: Handle any errors that occur during the data retrieval process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "data": [ - { - "card_number": "3998-2139-0328-0697", - "email": "c9a6c9555060@82c092e7.bd52", - "name": "82c092e7-74c0-4e60-bd52-c9a6c9555060", - "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" - }, - { - "card_number": "3562-0140-8820-7499", - "email": "6174366e2bc6@59f82e89.93fc", - "name": "59f82e89-138e-4f9b-93fc-6174366e2bc6", - "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" - } - ], - "errors": [] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -### Get By column name and column values - -Retrieve records by unique column values. Ideal for querying data without knowing Skyflow IDs, using alternate unique identifiers. - -#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/GetExample.java) of get call to retrieve data using column name and column values: - -```java -import com.skyflow.enums.RedactionType; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.data.GetResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to retrieve data from the Skyflow vault based on column values. - * - * 1. Initializes the Skyflow client with a given vault ID. - * 2. Creates a request to retrieve records based on specific column values (e.g., email addresses). - * 3. Prints the response to display the retrieved records after redacting sensitive data based on the specified redaction type. - */ -public class GetExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Initialize a list of column values (email addresses in this case) - ArrayList columnValues = new ArrayList<>(); - columnValues.add("john.doe@gmail.com"); // Example email address - columnValues.add("jane.doe@gmail.com"); // Example email address - - // Step 2: Create a GetRequest to retrieve records based on column values - // The request specifies: - // - `table`: The table from which the records will be retrieved - // - `columnName`: The column to filter the records by (e.g., "email") - // - `columnValues`: The list of values to match in the specified column - // - `redactionType`: Defines how sensitive data should be redacted (set to PLAIN_TEXT here) - GetRequest getByColumnRequest = GetRequest.builder() - .table("table1") // Replace with the actual table name - .columnName("email") // The column name to filter by (e.g., "email") - .columnValues(columnValues) // The list of column values to match - .redactionType(RedactionType.PLAIN_TEXT) // Set the redaction type (e.g., PLAIN_TEXT) - .build(); - - // Step 3: Send the request to the Skyflow vault and retrieve the records - GetResponse getByColumnResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getByColumnRequest); // Replace with actual Vault ID - System.out.println(getByColumnResponse); // Print the response to the console - - } catch (SkyflowException e) { - // Step 4: Handle any errors that occur during the data retrieval process - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "data": [ - { - "card_number": "4555555555555553", - "email": "john.doe@gmail.com", - "name": "john doe", - "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" - }, - { - "card_number": "4555555555555559", - "email": "jane.doe@gmail.com", - "name": "jane doe", - "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" - } - ], - "errors": [] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -### Redaction types - -See [`RedactionType`](docs/api_reference.md#redactiontype) in the API Reference for all available values and their descriptions. - -## Update - -To update data in your vault, use the `update` method. [`UpdateRequest`](docs/api_reference.md#updaterequest) accepts the table name, data map, optional tokens, `returnTokens`, and `tokenMode`. Returns an [`UpdateResponse`](docs/api_reference.md#updateresponse) with the `skyflow_id` and (when `returnTokens=true`) a token per updated column. - -### Construct an update request - -```java -import com.skyflow.enums.TokenMode; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.UpdateRequest; -import com.skyflow.vault.data.UpdateResponse; - -import java.util.HashMap; - -/** - * This example demonstrates how to update records in the Skyflow vault by providing new data and/or tokenized values, along with corresponding UpdateRequest schema. - * - */ -public class UpdateSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Prepare the data to update in the vault - // Use a HashMap to store the data that will be updated in the specified table - HashMap data = new HashMap<>(); - data.put("skyflow_id", ""); // Skyflow ID for identifying the record to update - data.put("", ""); // Example of a column name and its value to update - data.put("", ""); // Another example of a column name and its value to update - - // Step 2: Prepare the tokens (if necessary) for certain columns that require tokenization - // Use a HashMap to specify columns that need tokens in the update request - HashMap tokens = new HashMap<>(); - tokens.put("", ""); // Example of a column name that should be tokenized - - // Step 3: Create an UpdateRequest to specify the update operation - // The request includes the table name, token mode, data, tokens, and the returnTokens flag - UpdateRequest updateRequest = UpdateRequest.builder() - .table("") // Replace with the actual table name to update - .tokenMode(TokenMode.ENABLE) // Specifies the tokenization mode (ENABLE means tokenization is applied) - .data(data) // The data to update in the record - .tokens(tokens) // The tokens associated with specific columns - .returnTokens(true) // Specify whether to return tokens in the response - .build(); - - // Step 4: Send the request to the Skyflow vault and update the record - UpdateResponse updateResponse = skyflowClient.vault("").update(updateRequest); // Replace with actual Vault ID - System.out.println(updateResponse); // Print the response to confirm the update result - - } catch (SkyflowException e) { - // Step 5: Handle any errors that occur during the update operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/UpdateExample.java) of update call - -```java -import com.skyflow.enums.TokenMode; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.UpdateRequest; -import com.skyflow.vault.data.UpdateResponse; - -import java.util.HashMap; - -/** - * This example demonstrates how to update a record in the Skyflow vault with specified data and tokens. - * - * 1. Initializes the Skyflow client with a given vault ID. - * 2. Constructs an update request with data to modify and tokens to include. - * 3. Sends the request to update the record in the vault. - * 4. Prints the response to confirm the success or failure of the update operation. - */ -public class UpdateExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Prepare the data to update in the vault - // A HashMap is used to store the data that will be updated in the specified table - HashMap data = new HashMap<>(); - data.put("skyflow_id", "5b699e2c-4301-4f9f-bcff-0a8fd3057413"); // Skyflow ID identifies the record to update - data.put("name", "john doe"); // Updating the "name" column with a new value - data.put("card_number", "4111111111111115"); // Updating the "card_number" column with a new value - - // Step 2: Prepare the tokens to include in the update request - // Tokens can be included to update sensitive data with tokenized values - HashMap tokens = new HashMap<>(); - tokens.put("name", "72b8ffe3-c8d3-4b4f-8052-38b2a7405b5a"); // Tokenized value for the "name" column - - // Step 3: Create an UpdateRequest to define the update operation - // The request specifies the table name, token mode, data, and tokens for the update - UpdateRequest updateRequest = UpdateRequest.builder() - .table("table1") // Replace with the actual table name to update - .tokenMode(TokenMode.ENABLE) // Token mode enabled to allow tokenization of sensitive data - .data(data) // The data to update in the record - .tokens(tokens) // The tokenized values for sensitive columns - .build(); - - // Step 4: Send the update request to the Skyflow vault - UpdateResponse updateResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").update(updateRequest); // Replace with your actual Vault ID - System.out.println(updateResponse); // Print the response to confirm the update result - - } catch (SkyflowException e) { - // Step 5: Handle any exceptions that occur during the update operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging purposes - } - } -} -``` - -Sample response: - -- When `returnTokens` is set to `true` - -```json -{ - "skyflowId": "5b699e2c-4301-4f9f-bcff-0a8fd3057413", - "name": "72b8ffe3-c8d3-4b4f-8052-38b2a7405b5a", - "card_number": "4315-7650-1359-9681" -} -``` - -- When `returnTokens` is set to `false` - -```json -{ - "skyflowId": "5b699e2c-4301-4f9f-bcff-0a8fd3057413" -} -``` - -## Delete - -To delete records using Skyflow IDs, use the `delete` method. [`DeleteRequest`](docs/api_reference.md#deleterequest) accepts a table name and list of Skyflow IDs. Returns a [`DeleteResponse`](docs/api_reference.md#deleteresponse). - -### Construct a delete request - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.DeleteRequest; -import com.skyflow.vault.data.DeleteResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to delete records from a Skyflow vault using specified Skyflow IDs, along with corresponding DeleteRequest schema. - * - */ -public class DeleteSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Prepare a list of Skyflow IDs for the records to delete - // The list stores the Skyflow IDs of the records that need to be deleted from the vault - ArrayList ids = new ArrayList<>(); - ids.add(""); // Replace with actual Skyflow ID 1 - ids.add(""); // Replace with actual Skyflow ID 2 - ids.add(""); // Replace with actual Skyflow ID 3 - - // Step 2: Create a DeleteRequest to define the delete operation - // The request specifies the table from which to delete the records and the IDs of the records to delete - DeleteRequest deleteRequest = DeleteRequest.builder() - .ids(ids) // List of Skyflow IDs to delete - .table("") // Replace with the actual table name from which to delete - .build(); - - // Step 3: Send the delete request to the Skyflow vault - DeleteResponse deleteResponse = skyflowClient.vault("").delete(deleteRequest); // Replace with your actual Vault ID - System.out.println(deleteResponse); // Print the response to confirm the delete result - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the delete operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging purposes - } - } -} -``` - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/DeleteExample.java) of delete call: - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.DeleteRequest; -import com.skyflow.vault.data.DeleteResponse; - -import java.util.ArrayList; - -/** - * This example demonstrates how to delete records from a Skyflow vault using specified Skyflow IDs. - * - * 1. Initializes the Skyflow client with a given Vault ID. - * 2. Constructs a delete request by specifying the IDs of the records to delete. - * 3. Sends the delete request to the Skyflow vault to delete the specified records. - * 4. Prints the response to confirm the success or failure of the delete operation. - */ -public class DeleteExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Prepare a list of Skyflow IDs for the records to delete - // The list stores the Skyflow IDs of the records that need to be deleted from the vault - ArrayList ids = new ArrayList<>(); - ids.add("9cbf66df-6357-48f3-b77b-0f1acbb69280"); // Replace with actual Skyflow ID 1 - ids.add("ea74bef4-f27e-46fe-b6a0-a28e91b4477b"); // Replace with actual Skyflow ID 2 - ids.add("47700796-6d3b-4b54-9153-3973e281cafb"); // Replace with actual Skyflow ID 3 - - // Step 2: Create a DeleteRequest to define the delete operation - // The request specifies the table from which to delete the records and the IDs of the records to delete - DeleteRequest deleteRequest = DeleteRequest.builder() - .ids(ids) // List of Skyflow IDs to delete - .table("table1") // Replace with the actual table name from which to delete - .build(); - - // Step 3: Send the delete request to the Skyflow vault - DeleteResponse deleteResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").delete(deleteRequest); // Replace with your actual Vault ID - System.out.println(deleteResponse); // Print the response to confirm the delete result - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the delete operation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging purposes - } - } -} -``` - -Sample response: - -```json -{ - "deletedIds": [ - "9cbf66df-6357-48f3-b77b-0f1acbb69280", - "ea74bef4-f27e-46fe-b6a0-a28e91b4477b", - "47700796-6d3b-4b54-9153-3973e281cafb" - ] -} -``` - -## Query - -To retrieve data with SQL queries, use the `query` method. [`QueryRequest`](docs/api_reference.md#queryrequest) accepts a `query` string. Returns a [`QueryResponse`](docs/api_reference.md#queryresponse). - -### Construct a query request - -Refer to [Query your data](https://docs.skyflow.com/query-data/) and [Execute Query](https://docs.skyflow.com/record/#QueryService_ExecuteQuery) for guidelines and restrictions on supported SQL statements, operators, and keywords. - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.QueryRequest; -import com.skyflow.vault.data.QueryResponse; - -/** - * This example demonstrates how to execute a custom SQL query on a Skyflow vault, along with QueryRequest schema. - * - */ -public class QuerySchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Define the SQL query to execute on the Skyflow vault - // Replace "" with the actual SQL query you want to run - String query = ""; // Example: "SELECT * FROM table1 WHERE column1 = 'value'" - - // Step 2: Create a QueryRequest with the specified SQL query - QueryRequest queryRequest = QueryRequest.builder() - .query(query) // SQL query to execute - .build(); - - // Step 3: Execute the query request on the specified Skyflow vault - QueryResponse queryResponse = skyflowClient.vault("").query(queryRequest); // Replace with your actual Vault ID - System.out.println(queryResponse); // Print the response containing the query results - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the query execution - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging - } - } -} -``` - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/QueryExample.java) of query call - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.QueryRequest; -import com.skyflow.vault.data.QueryResponse; - -/** - * This example demonstrates how to execute a SQL query on a Skyflow vault to retrieve data. - * - * 1. Initializes the Skyflow client with the Vault ID. - * 2. Constructs a query request with a specified SQL query. - * 3. Executes the query against the Skyflow vault. - * 4. Prints the response from the query execution. - */ -public class QueryExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Define the SQL query - // Example query: Retrieve all records from the "cards" table with a specific skyflow_id - String query = "SELECT * FROM cards WHERE skyflow_id='3ea3861-x107-40w8-la98-106sp08ea83f'"; - - // Step 2: Create a QueryRequest with the SQL query - QueryRequest queryRequest = QueryRequest.builder() - .query(query) // SQL query to execute - .build(); - - // Step 3: Execute the query request on the specified Skyflow vault - QueryResponse queryResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").query(queryRequest); // Vault ID: 9f27764a10f7946fe56b3258e117 - System.out.println(queryResponse); // Print the query response (contains query results) - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the query execution - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging - } - } -} -``` - -Sample response: - -```json -{ - "fields": [ - { - "card_number": "XXXXXXXXXXXX1112", - "name": "S***ar", - "skyflowId": "3ea3861-x107-40w8-la98-106sp08ea83f", - "tokenizedData": null - } - ] -} -``` - -> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. - -## Upload File - -To upload files to a Skyflow vault, use the `uploadFile` method. [`FileUploadRequest`](docs/api_reference.md#fileuploadrequest) accepts the table name, column name, optional skyflow ID, and a file source (`fileObject`, `filePath`, or `base64`). Returns a [`FileUploadResponse`](docs/api_reference.md#fileuploadresponse). - -### Construct a file upload request - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.FileUploadRequest; -import com.skyflow.vault.data.FileUploadResponse; - -/** - * This example demonstrates how to upload a file to a Skyflow vault, along with the UploadFileRequest schema. - * - */ -public class UploadFileSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Specify file Object - File file = new File(""); - - // Step 2: Create an UploadFileRequest with the file details - FileUploadRequest uploadFileRequest = FileUploadRequest.builder() - .fileObject(file) // File object - .table("") // Vault table to upload into - .columnName("") // Column to assign to the uploaded file - .skyflowId("") // Skyflow id of the record - .build(); - - // Step 3: Execute the file upload request on the specified Skyflow vault - FileUploadResponse fileUploadResponse = skyflowClient.vault().uploadFile(uploadFileRequest); - System.out.println("File Upload Response: " + fileUploadResponse); - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions that occur during the upload - System.out.println("Error occurred during file upload:"); - e.printStackTrace(); // Print the exception stack trace for debugging - } - } -} - -``` - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/FileUploadExample.java) of file upload call -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.data.FileUploadRequest; -import com.skyflow.vault.data.FileUploadResponse; - -/** - * This example demonstrates how to upload a file to a Skyflow vault. - * - * 1. Initializes the Skyflow client with the Vault ID. - * 2. Constructs a file upload request with the file path, table name, and file name. - * 3. Executes the upload request against the Skyflow vault. - * 4. Prints the response from the upload. - */ -public class UploadFileExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Specify file Object - File file = new File("test/sample.txt"); - - // Step 2: Create an UploadFileRequest with the file details - FileUploadRequest uploadFileRequest = FileUploadRequest.builder() - .fileObject(file) // File object - .table("cards") // Vault table to upload into - .columnName("file") // Column to assign to the uploaded file - .skyflowId("c9312531-2087-439a-bd26-74c41f24db83") // Skyflow id of the record - .build(); - - // Step 3: Execute the file upload request - FileUploadResponse uploadResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").uploadFile(uploadFileRequest); - System.out.println("File Upload Response: " + fileUploadResponse); - - } catch (SkyflowException e) { - // Step 4: Handle any exceptions during the upload - System.out.println("Error occurred during file upload:"); - e.printStackTrace(); // Print exception details for debugging - } - } -} - -``` - -Sample response: - -```json -{ - "skyflowId": "c9312531-2087-439a-bd26-74c41f24db83", - "errors": null -} -``` - -# Detect -Skyflow Detect enables you to deidentify and reidentify sensitive data in text and files, supporting advanced privacy-preserving workflows. - -`DetectController` is the class returned by `skyflowClient.detect()` and `skyflowClient.detect(vaultId)`. - -```java -// Uses the default (first configured) vault -DetectController detect = skyflowClient.detect(); - -// Uses a specific vault by ID -DetectController detect = skyflowClient.detect(""); -``` - -**Methods:** - -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `deidentifyText(DeidentifyTextRequest)` | [`DeidentifyTextRequest`](docs/api_reference.md#deidentifytextrequest) | [`DeidentifyTextResponse`](docs/api_reference.md#deidentifytextresponse) | Deidentify sensitive entities in text | -| `reidentifyText(ReidentifyTextRequest)` | [`ReidentifyTextRequest`](docs/api_reference.md#reidentifytextrequest) | [`ReidentifyTextResponse`](docs/api_reference.md#reidentifytextresponse) | Restore original values from a deidentified text | -| `deidentifyFile(DeidentifyFileRequest)` | [`DeidentifyFileRequest`](docs/api_reference.md#deidentifyfilerequest) | [`DeidentifyFileResponse`](docs/api_reference.md#deidentifyfileresponse) | Deidentify sensitive data in a file | -| `getDetectRun(GetDetectRunRequest)` | [`GetDetectRunRequest`](docs/api_reference.md#getdetectrunrequest) | [`DeidentifyFileResponse`](docs/api_reference.md#deidentifyfileresponse) | Poll for the result of an async file deidentification | - -## Deidentify Text -To deidentify text, use the `deidentifyText` method. [`DeidentifyTextRequest`](docs/api_reference.md#deidentifytextrequest) accepts the text to deidentify along with optional entity types, regex lists, token format, and transformations. Returns a [`DeidentifyTextResponse`](docs/api_reference.md#deidentifytextresponse). - -### Construct an deidentify text request - -```java -import com.skyflow.enums.DetectEntities; -import com.skyflow.vault.detect.DateTransformation; -import com.skyflow.vault.detect.DeidentifyTextRequest; -import com.skyflow.vault.detect.TokenFormat; -import com.skyflow.vault.detect.Transformations; -import com.skyflow.vault.detect.DeidentifyTextResponse; - -import java.util.ArrayList; -import java.util.List; - -/** - * This example demonstrate to build deidentify text request. - */ -public class DeidentifyTextSchema { - - public static void main(String[] args) { - - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Configure the options for deidentify text - - // Replace with the entity you want to detect - List detectEntitiesList = new ArrayList<>(); - detectEntitiesList.add(DetectEntities.SSN); - - // Replace with the entity you want to detect with vault token - List vaultTokenList = new ArrayList<>(); - vaultTokenList.add(DetectEntities.CREDIT_CARD); - - // Replace with the entity you want to detect with entity only - List entityOnlyList = new ArrayList<>(); - entityOnlyList.add(DetectEntities.SSN); - - // Replace with the entity you want to detect with entity unique counter - List entityUniqueCounterList = new ArrayList<>(); - entityUniqueCounterList.add(DetectEntities.SSN); - - // Replace with the regex patterns you want to allow during deidentification - List allowRegexList = new ArrayList<>(); - allowRegexList.add(""); - - // Replace with the regex patterns you want to restrict during deidentification - List restrictRegexList = new ArrayList<>(); - restrictRegexList.add("YOUR_RESTRICT_REGEX_LIST"); - - // Configure Token Format - TokenFormat tokenFormat = TokenFormat.builder() - .vaultToken(vaultTokenList) - .entityOnly(entityOnlyList) - .entityUniqueCounter(entityUniqueCounterList) - .build(); - - // Configure Transformation - List detectEntitiesTransformationList = new ArrayList<>(); - detectEntitiesTransformationList.add(DetectEntities.DOB); // Replace with the entity you want to transform - - DateTransformation dateTransformation = new DateTransformation(20, 5, detectEntitiesTransformationList); - Transformations transformations = new Transformations(dateTransformation); - - // Step 3: Create a deidentify text request for the vault - DeidentifyTextRequest deidentifyTextRequest = DeidentifyTextRequest.builder() - .text("") // Replace with the text you want to deidentify - .entities(detectEntitiesList) - .allowRegexList(allowRegexList) - .restrictRegexList(restrictRegexList) - .tokenFormat(tokenFormat) - .transformations(transformations) - .build(); - - // Step 4: Use the Skyflow client to perform the deidentifyText operation - // Replace with your actual vault ID - DeidentifyTextResponse deidentifyTextResponse = skyflowClient.detect("").deidentifyText(deidentifyTextRequest); - - // Step 5: Print the response - System.out.println("Deidentify text Response: " + deidentifyTextResponse); - } -} - -``` - -## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/DeidentifyTextExample.java) of deidentify text: -```java -import java.util.ArrayList; -import java.util.List; - -import com.skyflow.enums.DetectEntities; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.detect.DateTransformation; -import com.skyflow.vault.detect.DeidentifyTextRequest; -import com.skyflow.vault.detect.DeidentifyTextResponse; -import com.skyflow.vault.detect.TokenFormat; -import com.skyflow.vault.detect.Transformations; - -/** - * Skyflow Deidentify Text Example - *

- * This example demonstrates how to use the Skyflow SDK to deidentify text data - * across multiple vaults. It includes: - * 1. Setting up credentials and vault configurations. - * 2. Creating a Skyflow client with multiple vaults. - * 3. Performing deidentify of text with various options. - * 4. Handling responses and errors. - */ - -public class DeidentifyTextExample { - public static void main(String[] args) throws SkyflowException { - - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Configuring the different options for deidentify - - // Replace with the entity you want to detect - List detectEntitiesList = new ArrayList<>(); - detectEntitiesList.add(DetectEntities.SSN); - detectEntitiesList.add(DetectEntities.CREDIT_CARD); - - // Replace with the entity you want to detect with vault token - List vaultTokenList = new ArrayList<>(); - vaultTokenList.add(DetectEntities.SSN); - vaultTokenList.add(DetectEntities.CREDIT_CARD); - - // Configure Token Format - TokenFormat tokenFormat = TokenFormat.builder() - .vaultToken(vaultTokenList) - .build(); - - // Configure Transformation for deidentified entities - List detectEntitiesTransformationList = new ArrayList<>(); - detectEntitiesTransformationList.add(DetectEntities.DOB); // Replace with the entity you want to transform - - DateTransformation dateTransformation = new DateTransformation(20, 5, detectEntitiesTransformationList); - Transformations transformations = new Transformations(dateTransformation); - - // Step 3: invoking Deidentify text on the vault - try { - // Create a deidentify text request for the vault - DeidentifyTextRequest deidentifyTextRequest = DeidentifyTextRequest.builder() - .text("My SSN is 123-45-6789 and my card is 4111 1111 1111 1111.") // Replace with your deidentify text - .entities(detectEntitiesList) - .tokenFormat(tokenFormat) - .transformations(transformations) - .build(); - // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id - DeidentifyTextResponse deidentifyTextResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").deidentifyText(deidentifyTextRequest); - - System.out.println("Deidentify text Response: " + deidentifyTextResponse); - } catch (SkyflowException e) { - System.err.println("Error occurred during deidentify: "); - e.printStackTrace(); // Print the exception for debugging purposes - } - } -} -``` - -Sample Response: -```json -{ - "processedText": "My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].", - "entities": [ - { - "token": "SSN_IWdexZe", - "value": "123-45-6789", - "textIndex": { - "start": 10, - "end": 21 - }, - "processedIndex": { - "start": 10, - "end": 23 - }, - "entity": "SSN", - "scores": { - "SSN": 0.9384 - } - }, - { - "token": "CREDIT_CARD_rUzMjdQ", - "value": "4111 1111 1111 1111", - "textIndex": { - "start": 37, - "end": 56 - }, - "processedIndex": { - "start": 39, - "end": 60 - }, - "entity": "CREDIT_CARD", - "scores": { - "CREDIT_CARD": 0.9051 - } - } - ], - "wordCount": 9, - "charCount": 57 -} -``` - -## Reidentify Text -To reidentify text, use the `reidentifyText` method. [`ReidentifyTextRequest`](docs/api_reference.md#reidentifytextrequest) accepts the redacted/deidentified text and optional entity lists controlling which entities to reveal, mask, or keep redacted. Returns a [`ReidentifyTextResponse`](docs/api_reference.md#reidentifytextresponse). - -### Construct an reidentify text request - -```java -import com.skyflow.enums.DetectEntities; -import com.skyflow.vault.detect.ReidentifyTextRequest; -import com.skyflow.vault.detect.ReidentifyTextResponse; - -import java.util.ArrayList; -import java.util.List; - -/** - * This example demonstrates how to build a reidentify text request. - */ -public class ReidentifyTextSchema { - public static void main(String[] args) { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Configuring the different options for reidentify - List maskedEntity = new ArrayList<>(); - maskedEntity.add(DetectEntities.CREDIT_CARD); // Replace with the entity you want to mask - - List plainTextEntity = new ArrayList<>(); - plainTextEntity.add(DetectEntities.SSN); // Replace with the entity you want to keep in plain text - - // List redactedEntity = new ArrayList<>(); - // redactedEntity.add(DetectEntities.SSN); // Replace with the entity you want to redact - - - // Step 3: Create a reidentify text request with the configured entities - ReidentifyTextRequest reidentifyTextRequest = ReidentifyTextRequest.builder() - .text("My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].") // Replace with your deidentify text - .maskedEntities(maskedEntity) -// .redactedEntities(redactedEntity) - .plainTextEntities(plainTextEntity) - .build(); - - // Step 4: Invoke reidentify text on the vault - ReidentifyTextResponse reidentifyTextResponse = skyflowClient.detect("").reidentifyText(reidentifyTextRequest); - System.out.println("Reidentify text Response: " + reidentifyTextResponse); - } -} -``` - -## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/ReidentifyTextExample.java) of Reidentify text - -```java -import com.skyflow.enums.DetectEntities; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.detect.ReidentifyTextRequest; -import com.skyflow.vault.detect.ReidentifyTextResponse; - -import java.util.ArrayList; -import java.util.List; - -/** - * Skyflow Reidentify Text Example - *

- * This example demonstrates how to use the Skyflow SDK to reidentify text data - * across multiple vaults. It includes: - * 1. Setting up credentials and vault configurations. - * 2. Creating a Skyflow client with multiple vaults. - * 3. Performing reidentify of text with various options. - * 4. Handling responses and errors. - */ - -public class ReidentifyTextExample { - public static void main(String[] args) throws SkyflowException { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Configuring the different options for reidentify - List maskedEntity = new ArrayList<>(); - maskedEntity.add(DetectEntities.CREDIT_CARD); // Replace with the entity you want to mask - - List plainTextEntity = new ArrayList<>(); - plainTextEntity.add(DetectEntities.SSN); // Replace with the entity you want to keep in plain text - - try { - // Step 3: Create a reidentify text request with the configured options - ReidentifyTextRequest reidentifyTextRequest = ReidentifyTextRequest.builder() - .text("My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].") // Replace with your deidentify text - .maskedEntities(maskedEntity) - .plainTextEntities(plainTextEntity) - .build(); - - // Step 4: Invoke Reidentify text on the vault - // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id - ReidentifyTextResponse reidentifyTextResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").reidentifyText(reidentifyTextRequest); - - // Handle the response from the reidentify text request - System.out.println("Reidentify text Response: " + reidentifyTextResponse); - } catch (SkyflowException e) { - System.err.println("Error occurred during reidentify : "); - e.printStackTrace(); - } - } -} -``` - -Sample Response: - -```json -{ - "processedText":"My SSN is 123-45-6789 and my card is XXXXX1111." -} -``` - -## Deidentify file -To deidentify files, use the `deidentifyFile` method. [`DeidentifyFileRequest`](docs/api_reference.md#deidentifyfilerequest) accepts a [`FileInput`](docs/api_reference.md#fileinput) and optional parameters controlling entity detection, masking, output format, and async wait time. Supports images, PDFs, audio, documents, spreadsheets, and presentations. Returns a [`DeidentifyFileResponse`](docs/api_reference.md#deidentifyfileresponse). - -### AudioBleep - -[`AudioBleep`](docs/api_reference.md#audiobleep) controls how detected sensitive audio segments are replaced with a bleep tone. Used in `DeidentifyFileRequest.builder().bleep(audioBleep)` for audio files. - -```java -import com.skyflow.vault.detect.AudioBleep; - -AudioBleep audioBleep = AudioBleep.builder() - .frequency(1000D) // bleep tone frequency in Hz - .gain(0.5D) // bleep tone gain (volume level) - .startPadding(0.2D) // silence padding before the bleep (seconds) - .stopPadding(0.2D) // silence padding after the bleep (seconds) - .build(); -``` - -### Construct an deidentify file request - -```java -import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; -import com.skyflow.enums.LogLevel; -import com.skyflow.enums.MaskingMethod; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.detect.DeidentifyFileRequest; -import com.skyflow.vault.detect.DeidentifyFileResponse; - -import java.io.File; - -/** - * This example demonstrates how to build a deidentify file request. - */ - -public class DeidentifyFileSchema { - - public static void main(String[] args) { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Create a deidentify file request with all options - - // Create file object - File file = new File(""); // Replace with the path to the file you want to deidentify - - // Create file input using the file object - FileInput fileInput = FileInput.builder() - .file(file) - // .filePath("") // Alternatively, you can use .filePath() - .build(); - - // Output configuration - String outputDirectory = ""; // Replace with the desired output directory to save the deidentified file - - // Entities to detect - // List detectEntities = new ArrayList<>(); - // detectEntities.add(DetectEntities.IP_ADDRESS); // Replace with the entities you want to detect - - // Image-specific options - // Boolean outputProcessedImage = true; // Include processed image in output - // Boolean outputOcrText = true; // Include OCR text in output - MaskingMethod maskingMethod = MaskingMethod.BLACKBOX; // Masking method for images - - // PDF-specific options - // Integer pixelDensity = 15; // Pixel density for PDF processing - // Integer maxResolution = 2000; // Max resolution for PDF - - // Audio-specific options - // Boolean outputProcessedAudio = true; // Include processed audio - // DetectOutputTranscriptions outputTanscription = DetectOutputTranscriptions.PLAINTEXT_TRANSCRIPTION; // Transcription type - - // Audio bleep configuration - // AudioBleep audioBleep = AudioBleep.builder() - // .frequency(5D) // Pitch in Hz - // .startPadding(7D) // Padding at start (seconds) - // .stopPadding(8D) // Padding at end (seconds) - // .build(); - - Integer waitTime = 20; // Max wait time for response (max 64 seconds) - - DeidentifyFileRequest deidentifyFileRequest = DeidentifyFileRequest.builder() - .file(fileInput) - .waitTime(waitTime) - .entities(detectEntities) - .outputDirectory(outputDirectory) - .maskingMethod(maskingMethod) - // .outputProcessedImage(outputProcessedImage) - // .outputOcrText(outputOcrText) - // .pixelDensity(pixelDensity) - // .maxResolution(maxResolution) - // .outputProcessedAudio(outputProcessedAudio) - // .outputTranscription(outputTanscription) - // .bleep(audioBleep) - .build(); - - - DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("").deidentifyFile(deidentifyFileRequest); - System.out.println("Deidentify file response: " + deidentifyFileResponse.toString()); - } -} -``` - -## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/DeidentifyFileExample.java) of Deidentify file - -```java -import java.io.File; - -import com.skyflow.enums.MaskingMethod; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.detect.DeidentifyFileRequest; -import com.skyflow.vault.detect.DeidentifyFileResponse; - -/** - * Skyflow Deidentify File Example - *

- * This example demonstrates how to use the Skyflow SDK to deidentify file - * It has all available options for deidentifying files. - * Supported file types: images (jpg, png, etc.), pdf, audio (mp3, wav), documents, spreadsheets, presentations, structured text. - * It includes: - * 1. Configure credentials - * 2. Set up vault configuration - * 3. Create a deidentify file request with all options - * 4. Call deidentifyFile to deidentify file. - * 5. Handle response and errors - */ -public class DeidentifyFileExample { - - public static void main(String[] args) throws SkyflowException { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - try { - // Step 2: Create a deidentify file request with all options - - - // Create file object - File file = new File("sensitive-folder/personal-info.txt"); // Replace with the path to the file you want to deidentify - - // Create file input using the file object - FileInput fileInput = FileInput.builder() - .file(file) - // .filePath("") // Alternatively, you can use .filePath() - .build(); - - // Output configuration - String outputDirectory = "deidentified-file/"; // Replace with the desired output directory to save the deidentified file - - // Entities to detect - // List detectEntities = new ArrayList<>(); - // detectEntities.add(DetectEntities.IP_ADDRESS); // Replace with the entities you want to detect - - // Image-specific options - // Boolean outputProcessedImage = true; // Include processed image in output - // Boolean outputOcrText = true; // Include OCR text in output - MaskingMethod maskingMethod = MaskingMethod.BLACKBOX; // Masking method for images - - Integer waitTime = 20; // Max wait time for response (max 64 seconds) - - DeidentifyFileRequest deidentifyFileRequest = DeidentifyFileRequest.builder() - .file(fileInput) - .waitTime(waitTime) - .outputDirectory(outputDirectory) - .maskingMethod(maskingMethod) - .build(); - - // Step 3: Invoking deidentifyFile - // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id - DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").deidentifyFile(deidentifyFileRequest); - System.out.println("Deidentify file response: " + deidentifyFileResponse.toString()); - } catch (SkyflowException e) { - System.err.println("Error occurred during deidentify file: "); - e.printStackTrace(); - } - } -} - -``` - -Sample response: - -```json -{ - "file": { - "name": "deidentified.txt", - "size": 33, - "type": "", - "lastModified": 1751355183039 - }, - "fileBase64": "bXkgY2FyZCBudW1iZXIgaXMgW0NSRURJVF", - "type": "redacted_file", - "extension": "txt", - "wordCount": 11, - "charCount": 61, - "sizeInKb": 0, - "entities": [ - { - "file": "bmFtZTogW05BTUVfMV0gCm==", - "type": "entities", - "extension": "json" - } - ], - "runId": "undefined", - "status": "success" -} - -``` - -**Supported file types:** -- Documents: `doc`, `docx`, `pdf` -- PDFs: `pdf` -- Images: `bmp`, `jpeg`, `jpg`, `png`, `tif`, `tiff` -- Structured text: `json`, `xml` -- Spreadsheets: `csv`, `xls`, `xlsx` -- Presentations: `ppt`, `pptx` -- Audio: `mp3`, `wav` - -**Note:** -- Transformations cannot be applied to Documents, Images, or PDFs file formats. - -- The `waitTime` option must be ≤ 64 seconds; otherwise, an error is thrown. - -- If the API takes more than 64 seconds to process the file, it will return only the run ID in the response. - -Sample response (when the API takes more than 64 seconds): -```json -{ - "file": null, - "fileBase64": null, - "type": null, - "extension": null, - "wordCount": null, - "charCount": null, - "sizeInKb": null, - "durationInSeconds": null, - "pageCount": null, - "slideCount": null, - "entities": null, - "runId": "1273a8c6-c498-4293-a9d6-389864cd3a44", - "status": "IN_PROGRESS", - "errors": null -} -``` - -## Get run: -To retrieve the results of a previously started file deidentification operation, use the `getDetectRun` method. [`GetDetectRunRequest`](docs/api_reference.md#getdetectrunrequest) accepts the `runId` returned from a prior `deidentifyFile` call. Returns a [`DeidentifyFileResponse`](docs/api_reference.md#deidentifyfileresponse). - -### Construct an get run request - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.detect.DeidentifyFileResponse; -import com.skyflow.vault.detect.GetDetectRunRequest; - -/** - * Skyflow Get Detect Run Example - */ - -public class GetDetectRunSchema { - - public static void main(String[] args) { - try { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - - // Step 2: Create a get detect run request - GetDetectRunRequest getDetectRunRequest = GetDetectRunRequest.builder() - .runId("") // Replace with the runId from deidentifyFile call - .build(); - - // Step 3: Call getDetectRun to poll for file processing results - // Replace with your actual vault ID - DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("").getDetectRun(getDetectRunRequest); - System.out.println("Get Detect Run Response: " + deidentifyFileResponse); - } catch (SkyflowException e) { - System.err.println("Error occurred during get detect run: "); - e.printStackTrace(); - } - } -} - -``` - -## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/GetDetectRunExample.java) of get run -```java -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.detect.DeidentifyFileResponse; -import com.skyflow.vault.detect.GetDetectRunRequest; - -/** - * Skyflow Get Detect Run Example - *

- * This example demonstrates how to: - * 1. Configure credentials - * 2. Set up vault configuration - * 3. Create a get detect run request - * 4. Call getDetectRun to poll for file processing results - * 5. Handle response and errors - */ -public class GetDetectRunExample { - public static void main(String[] args) throws SkyflowException { - // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. - try { - - // Step 2: Create a get detect run request - GetDetectRunRequest getDetectRunRequest = GetDetectRunRequest.builder() - .runId("e0038196-4a20-422b-bad7-e0477117f9bb") // Replace with the runId from deidentifyFile call - .build(); - - // Step 3: Call getDetectRun to poll for file processing results - // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id - DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").getDetectRun(getDetectRunRequest); - System.out.println("Get Detect Run Response: " + deidentifyFileResponse); - } catch (SkyflowException e) { - System.err.println("Error occurred during get detect run: "); - e.printStackTrace(); - } - } -} -``` - -Sample Response: - -```json -{ - "file": "bmFtZTogW05BTET0JfMV0K", - "type": "redacted_file", - "extension": "txt", - "wordCount": 11, - "charCount": 61, - "sizeInKb": 0.0, - "entities": [ - { - "file": "gW05BTUVfMV0gCmNhcmQ0K", - "type": "entities", - "extension": "json" - } - ], - "runId": "e0038196-4a20-422b-bad7-e0477117f9bb", - "status": "success" -} - -``` - -## Detect response types - -The Detect API returns structured objects for detected entities. See the API Reference for full attribute lists: [`EntityInfo`](docs/api_reference.md#entityinfo), [`TextIndex`](docs/api_reference.md#textindex), [`FileEntityInfo`](docs/api_reference.md#fileentityinfo), [`FileInfo`](docs/api_reference.md#fileinfo). - -### EntityInfo and TextIndex - -[`EntityInfo`](docs/api_reference.md#entityinfo) appears in `DeidentifyTextResponse.getEntities()`. Each entry includes the detected entity type, original value, replacement token, character positions ([`TextIndex`](docs/api_reference.md#textindex)), and confidence scores. - -```java -DeidentifyTextResponse response = skyflowClient.detect("").deidentifyText(request); - -for (EntityInfo entity : response.getEntities()) { - System.out.println("Entity : " + entity.getEntity()); - System.out.println("Value : " + entity.getValue()); - System.out.println("Token : " + entity.getToken()); - System.out.println("Start : " + entity.getTextIndex().getStart()); - System.out.println("End : " + entity.getTextIndex().getEnd()); - System.out.println("Score : " + entity.getScores().get(entity.getEntity())); -} -``` - -### FileEntityInfo and FileInfo - -[`FileEntityInfo`](docs/api_reference.md#fileentityinfo) appears in `DeidentifyFileResponse.getEntities()`. [`FileInfo`](docs/api_reference.md#fileinfo) is returned by `DeidentifyFileResponse.getFile()` and contains file metadata. - -## Detect enums - -See the API Reference for full value descriptions: [`TokenType`](docs/api_reference.md#tokentype), [`DeidentifyFileStatus`](docs/api_reference.md#deidentifyfilestatus), [`DetectOutputTranscriptions`](docs/api_reference.md#detectoutputtranscriptions), [`MaskingMethod`](docs/api_reference.md#maskingmethod), [`DetectEntities`](docs/api_reference.md#detectentities). - -### TokenType - -[`TokenType`](docs/api_reference.md#tokentype) controls how detected entities are tokenized. Used in `TokenFormat.builder()`. - -```java -import com.skyflow.enums.TokenType; - -TokenFormat tokenFormat = TokenFormat.builder() - .vaultToken(vaultTokenList) // uses VAULT_TOKEN - .entityOnly(entityOnlyList) // uses ENTITY_ONLY - .entityUniqueCounter(entityUniqueCounterList) // uses ENTITY_UNIQUE_COUNTER - .build(); -``` - -### DeidentifyFileStatus - -[`DeidentifyFileStatus`](docs/api_reference.md#deidentifyfilestatus) is returned in `DeidentifyFileResponse.getStatus()` to indicate async processing state. - -```java -import com.skyflow.enums.DeidentifyFileStatus; - -DeidentifyFileResponse response = skyflowClient.detect("").getDetectRun(request); -if (DeidentifyFileStatus.SUCCESS.value().equals(response.getStatus())) { - // safe to read response.getFile() -} else if (DeidentifyFileStatus.IN_PROGRESS.value().equals(response.getStatus())) { - // poll again using the runId -} -``` - -### DetectOutputTranscriptions - -[`DetectOutputTranscriptions`](docs/api_reference.md#detectoutputtranscriptions) controls the transcription format for audio file deidentification. - -```java -import com.skyflow.enums.DetectOutputTranscriptions; - -DeidentifyFileRequest request = DeidentifyFileRequest.builder() - .file(fileInput) - .outputTranscription(DetectOutputTranscriptions.TRANSCRIPTION) - .build(); -``` - -# Connections - -Skyflow Connections is a gateway service that uses tokenization to securely send and receive data between your systems and first- or third-party services. The [connections](https://github.com/skyflowapi/skyflow-java/tree/main/src/main/java/com/skyflow/vault/connection) module invokes both inbound and/or outbound connections. - -- **Inbound connections**: Act as intermediaries between your client and server, tokenizing sensitive data before it reaches your backend, ensuring downstream services handle only tokenized data. -- **Outbound connections**: Enable secure extraction of data from the vault and transfer it to third-party services via your backend server, such as processing checkout or card issuance flows. - -## ConnectionController - -`ConnectionController` is the class returned by `skyflowClient.connection()` and `skyflowClient.connection(connectionId)`. All connection operations are called on this object. - -```java -// Uses the default (first configured) connection -ConnectionController connection = skyflowClient.connection(); - -// Uses a specific connection by ID -ConnectionController connection = skyflowClient.connection(""); -``` - -**Methods:** - -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `invoke(InvokeConnectionRequest)` | [`InvokeConnectionRequest`](docs/api_reference.md#invokeconnectionrequest) | [`InvokeConnectionResponse`](docs/api_reference.md#invokeconnectionresponse) | Invoke an inbound or outbound connection | - -## Invoke a connection - -To invoke a connection, use the `invoke` method of the Skyflow client. - -### Construct an invoke connection request - -```java -import com.skyflow.enums.RequestMethod; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.connection.InvokeConnectionRequest; -import com.skyflow.vault.connection.InvokeConnectionResponse; - -import java.util.HashMap; -import java.util.Map; - -/** - * This example demonstrates how to invoke an external connection using the Skyflow SDK, along with corresponding InvokeConnectionRequest schema. - * - */ -public class InvokeConnectionSchema { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Define the request body parameters - // These are the values you want to send in the request body - Map requestBody = new HashMap<>(); - requestBody.put("", ""); - requestBody.put("", ""); - - // Step 2: Define the request headers - // Add any required headers that need to be sent with the request - Map requestHeaders = new HashMap<>(); - requestHeaders.put("", ""); - requestHeaders.put("", ""); - - // Step 3: Define the path parameters - // Path parameters are part of the URL and typically used in RESTful APIs - Map pathParams = new HashMap<>(); - pathParams.put("", ""); - pathParams.put("", ""); - - // Step 4: Define the query parameters - // Query parameters are included in the URL after a '?' and are used to filter or modify the response - Map queryParams = new HashMap<>(); - queryParams.put("", ""); - queryParams.put("", ""); - - // Step 5: Build the InvokeConnectionRequest using the provided parameters - InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() - .method(RequestMethod.POST) // The HTTP method to use for the request (POST in this case) - .requestBody(requestBody) // The body of the request - .requestHeaders(requestHeaders) // The headers to include in the request - .pathParams(pathParams) // The path parameters for the URL - .queryParams(queryParams) // The query parameters to append to the URL - .build(); - - // Step 6: Invoke the connection using the request - // Replace "" with the actual connection ID you are using - InvokeConnectionResponse invokeConnectionResponse = skyflowClient.connection("").invoke(invokeConnectionRequest); - - // Step 7: Print the response from the invoked connection - // This response contains the result of the request sent to the external system - System.out.println(invokeConnectionResponse); - - } catch (SkyflowException e) { - // Step 8: Handle any exceptions that occur during the connection invocation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging - } - } -} -``` - -`method` accepts any [`RequestMethod`](docs/api_reference.md#requestmethod) value (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`). See [`InvokeConnectionRequest`](docs/api_reference.md#invokeconnectionrequest) in the API Reference for all builder options. - -**pathParams, queryParams, requestHeader, requestBody** are the JSON objects represented as HashMaps, that will be sent through the connection integration url. - -### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/connection/InvokeConnectionExample.java) of invokeConnection - -```java -import com.skyflow.Skyflow; -import com.skyflow.config.ConnectionConfig; -import com.skyflow.config.Credentials; -import com.skyflow.enums.LogLevel; -import com.skyflow.enums.RequestMethod; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.connection.InvokeConnectionRequest; -import com.skyflow.vault.connection.InvokeConnectionResponse; - -import java.util.HashMap; -import java.util.Map; - -/** - * This example demonstrates how to invoke an external connection using the Skyflow SDK. - * It configures a connection, sets up the request, and sends a POST request to the external service. - * - * 1. Initialize Skyflow client with connection details. - * 2. Define the request body, headers, and method. - * 3. Execute the connection request. - * 4. Print the response from the invoked connection. - */ -public class InvokeConnectionExample { - public static void main(String[] args) { - try { - // Initialize Skyflow client - // Step 1: Set up credentials and connection configuration - // Load credentials from a JSON file (you need to provide the correct path) - Credentials credentials = new Credentials(); - credentials.setPath("/path/to/credentials.json"); - - // Define the connection configuration (URL and credentials) - ConnectionConfig connectionConfig = new ConnectionConfig(); - connectionConfig.setConnectionId(""); // Replace with actual connection ID - connectionConfig.setConnectionUrl("https://connection.url.com"); // Replace with actual connection URL - connectionConfig.setCredentials(credentials); // Set credentials for the connection - - // Initialize the Skyflow client with the connection configuration - Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.DEBUG) // Set log level to DEBUG for detailed logs - .addConnectionConfig(connectionConfig) // Add connection configuration to client - .build(); // Build the Skyflow client instance - - // Step 2: Define the request body and headers - // Map for request body parameters - Map requestBody = new HashMap<>(); - requestBody.put("card_number", "4337-1696-5866-0865"); // Example card number - requestBody.put("ssn", "524-41-4248"); // Example SSN - - // Map for request headers - Map requestHeaders = new HashMap<>(); - requestHeaders.put("Content-Type", "application/json"); // Set content type for the request - - // Step 3: Build the InvokeConnectionRequest with required parameters - // Set HTTP method to POST, include the request body and headers - InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() - .method(RequestMethod.POST) // HTTP POST method - .requestBody(requestBody) // Add request body parameters - .requestHeaders(requestHeaders) // Add headers - .build(); // Build the request - - // Step 4: Invoke the connection and capture the response - // Replace "" with the actual connection ID - InvokeConnectionResponse invokeConnectionResponse = skyflowClient.connection("").invoke(invokeConnectionRequest); - - // Step 5: Print the response from the connection invocation - System.out.println(invokeConnectionResponse); // Print the response to the console - - } catch (SkyflowException e) { - // Step 6: Handle any exceptions that occur during the connection invocation - System.out.println("Error occurred: "); - e.printStackTrace(); // Print the exception stack trace for debugging - } - } -} -``` - -Sample response: - -```json -{ - "data": { - "card_number": "4337-1696-5866-0865", - "ssn": "524-41-4248" - }, - "metadata": { - "requestId": "4a3453b5-7aa4-4373-98d7-cf102b1f6f97" - } -} -``` - -# Authenticate with bearer tokens - -This section covers methods for generating and managing tokens to authenticate API calls: - -- **Generate a bearer token**: - Enable the creation of bearer tokens using service account credentials. These tokens, valid for 60 minutes, provide secure access to Vault services and management APIs based on the service account's permissions. Use this for general API calls when you only need basic authentication without additional context or role-based restrictions. -- **Generate a bearer token with context**: - Support embedding context values into bearer tokens, enabling dynamic access control and the ability to track end-user identity. These tokens include context claims and allow flexible authorization for Vault services. Use this when policies depend on specific contextual attributes or when tracking end-user identity is required. -- **Generate a scoped bearer token**: - Facilitate the creation of bearer tokens with role-specific access, ensuring permissions are limited to the operations allowed by the designated role. This is particularly useful for service accounts with multiple roles. Use this to enforce fine-grained role-based access control, ensuring tokens only grant permissions for a specific role. -- **Generate signed data tokens**: - Add an extra layer of security by digitally signing data tokens with the service account's private key. These signed tokens can be securely detokenized, provided the necessary bearer token and permissions are available. Use this to add cryptographic protection to sensitive data, enabling secure detokenization with verified integrity and authenticity. - -## Generate a bearer token - -The [Service Account](https://github.com/skyflowapi/skyflow-java/tree/main/src/main/java/com/skyflow/serviceaccount/util) Java module generates service account tokens using a service account credentials file, which is provided when a service account is created. The tokens generated by this module are valid for 60 minutes and can be used to make API calls to the [Data](https://docs.skyflow.com/record/) and [Management](https://docs.skyflow.com/management/) APIs, depending on the permissions assigned to the service account. - -The `BearerToken` utility class generates bearer tokens using a credentials JSON file. Alternatively, you can pass the credentials as a string. - -[Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java): - -```java -/** - * 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(); - } - } -} -``` - -## Generate bearer tokens with context - -**Context-aware authorization** embeds context values into a bearer token during its generation so you can reference those values in your policies. This enables more flexible access controls, such as helping you track end-user identity when making API calls using service accounts, and facilitates using signed data tokens during detokenization. - -A service account with the `context_id` identifier generates bearer tokens containing context information, represented as a JWT claim in a Skyflow-generated bearer token. Tokens generated from such service accounts include a `context_identifier` claim, are valid for 60 minutes, and can be used to make API calls to the Data and Management APIs, depending on the service account's permissions. - -The `setCtx()` method accepts either a **String** or a **`Map`**: - -**String context** — use when your policy references a single context value: - -```java -BearerToken token = BearerToken.builder() - .setCredentials(new File(filePath)) - .setCtx("user_12345") - .build(); -``` - -**JSON object context** — use when your policy needs multiple context values for conditional data access. Each key in the `Map` maps to a Skyflow CEL policy variable under `request.context.*`: - -```java -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(); -``` - -With the map above, your Skyflow policies can reference `request.context.role`, `request.context.department`, and `request.context.user_id` to make conditional access decisions. - -You can also set context on `Credentials` for automatic token generation: - -```java -// String context -Credentials credentials = new Credentials(); -credentials.setPath("path/to/credentials.json"); -credentials.setContext("user_12345"); - -// Map context -Map ctx = new HashMap<>(); -ctx.put("role", "admin"); -ctx.put("department", "finance"); -credentials.setContext(ctx); -``` - -> **Note:** `getContext()` returns `Object` — callers should use `instanceof` if they need to inspect the type. - -Context map keys must contain only alphanumeric characters and underscores (`[a-zA-Z0-9_]`). Invalid keys will throw a `SkyflowException`. - -[Full example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java) - -See Skyflow's [context-aware authorization](https://docs.skyflow.com) and [conditional data access](https://docs.skyflow.com) docs for policy variable syntax like `request.context.*`. - -## Generate scoped bearer tokens - -A service account with multiple roles can generate bearer tokens with access limited to a specific role by specifying the appropriate `roleID`. This can be used to limit access to specific roles for services with multiple responsibilities, such as segregating access for billing and analytics. The generated bearer tokens are valid for 60 minutes and can only execute operations permitted by the permissions associated with the designated role. - -[Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java): - -```java -import com.skyflow.errors.SkyflowException; -import com.skyflow.serviceaccount.util.BearerToken; - -import java.io.File; -import java.util.ArrayList; - -/** - * Example program to generate a Scoped Token using Skyflow's BearerToken utility. - * The token is generated by providing the file path to the credentials.json file - * and specifying roles associated with the token. - */ -public class ScopedTokenGenerationExample { - public static void main(String[] args) { - // Variable to store the generated scoped token - String scopedToken = null; - - // Example: Generate Scoped Token by specifying the credentials.json file path - try { - // Create a list of roles that the generated token will be scoped to - ArrayList roles = new ArrayList<>(); - roles.add("ROLE_ID"); // Add a specific role to the list (e.g., "ROLE_ID") - - // Specify the full file path to the service account's credentials.json file - String filePath = ""; - - // Create a BearerToken object using the credentials file and associated roles - BearerToken bearerToken = BearerToken.builder() - .setCredentials(new File(filePath)) // Set credentials using the credentials.json file - .setRoles(roles) // Set the roles that the token should be scoped to - .build(); // Build the BearerToken object - - // Retrieve the generated scoped token - scopedToken = bearerToken.getBearerToken(); - - // Print the generated scoped token to the console - System.out.println(scopedToken); - } catch (SkyflowException e) { - // Handle exceptions that may occur during token generation - e.printStackTrace(); - } - } -} -``` - -Notes: - -- You can pass either the file path of a service account key credentials file or the service account key credentials as a string to the `setCredentials` method of the `BearerTokenBuilder` class. -- If both a file path and a string are provided, the last method used takes precedence. -- To generate multiple bearer tokens concurrently using threads, refer to the following [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java). - -## Generate Signed Data Tokens - -Skyflow generates data tokens when sensitive data is inserted into the vault. These data tokens can be digitally signed -with the private key of the service account credentials, which adds an additional layer of protection. Signed tokens can -be detokenized by passing the signed data token and a bearer token generated from service account credentials. The -service account must have appropriate permissions and context to detokenize the signed data tokens. - -The `setCtx()` method on `SignedDataTokensBuilder` also accepts either a **String** or a **`Map`**, using the same format as bearer tokens: - -```java -// String context -SignedDataTokens signedToken = SignedDataTokens.builder() - .setCredentials(new File(filePath)) - .setCtx("user_12345") - .setTimeToLive(30) - .setDataTokens(dataTokens) - .build(); - -// JSON object context -Map ctx = new HashMap<>(); -ctx.put("role", "analyst"); -ctx.put("department", "research"); - -SignedDataTokens signedToken = SignedDataTokens.builder() - .setCredentials(new File(filePath)) - .setCtx(ctx) - .setTimeToLive(30) - .setDataTokens(dataTokens) - .build(); -``` - -[Full example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java) - -Response: - -```json -[ - { - "dataToken": "5530-4316-0674-5748", - "signedDataToken": "signed_token_eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzLCpZjA" - } -] -``` - -Notes: - -- You can provide either the file path to a service account key credentials file or the service account key credentials as a string to the `setCredentials` method of the `SignedDataTokensBuilder` class. -- If both a file path and a string are passed to the `setCredentials` method, the most recently specified input takes precedence. -- The `time-to-live` (TTL) value should be specified in seconds. -- By default, the TTL value is set to 60 seconds. - -## Bearer token expiry edge case -When you use bearer tokens for authentication and API requests in SDKs, there's the potential for a token to expire after the token is verified as valid but before the actual API call is made, causing the request to fail unexpectedly due to the token's expiration. An error from this edge case would look something like this: - -```txt -message: Authentication failed. Bearer token is expired. Use a valid bearer token. See https://docs.skyflow.com/api-authentication/ -``` - -If you encounter this kind of error, retry the request. During the retry, the SDK detects that the previous bearer token has expired and generates a new one for the current and subsequent requests. - -#### [Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java): - -```java -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.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 DetokenizeExample { - 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 - ArrayList tokenList = new ArrayList<>(); - tokenList.add(""); // First token - tokenList.add(""); // Second token - - // Building a detokenization request with the token list and configuration - DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() - .tokens(tokenList) // Adding tokens to the request - .continueOnError(false) // Stop on error - .redactionType(RedactionType.PLAIN_TEXT) // Redaction type (e.g., PLAIN_TEXT) - .build(); - - // Sending the detokenization request and receiving the response - DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); - - // Printing the detokenized response - System.out.println(detokenizeResponse); - } -} -``` - -# Client Management - -After the `Skyflow` client is built you can add, retrieve, update, or remove vault and connection configurations at runtime — without rebuilding the client. - -## Vault configuration management - -```java -import com.skyflow.config.VaultConfig; - -// Add a new vault at runtime -skyflowClient.addVaultConfig(newVaultConfig); - -// Retrieve the config for a specific vault -VaultConfig config = skyflowClient.getVaultConfig(""); - -// Update an existing vault config (match by vaultId) -skyflowClient.updateVaultConfig(updatedVaultConfig); - -// Remove a vault from the client -skyflowClient.removeVaultConfig(""); -``` - -## Connection configuration management - -```java -import com.skyflow.config.ConnectionConfig; - -// Add a new connection at runtime -skyflowClient.addConnectionConfig(newConnectionConfig); - -// Retrieve the config for a specific connection -ConnectionConfig config = skyflowClient.getConnectionConfig(""); - -// Update an existing connection config (match by connectionId) -skyflowClient.updateConnectionConfig(updatedConnectionConfig); - -// Remove a connection from the client -skyflowClient.removeConnectionConfig(""); -``` - -## Credentials and log level management - -```java -// Replace the Skyflow-level credentials used when vault/connection configs -// do not specify their own credentials -skyflowClient.updateSkyflowCredentials(newCredentials); - -// Update the log level after the client has been built -skyflowClient.updateLogLevel(LogLevel.DEBUG); - -// Read the current log level -LogLevel currentLevel = skyflowClient.getLogLevel(); -``` - -**Client management method reference:** - -| Method | Returns | Description | -|--------|---------|-------------| -| `addVaultConfig(VaultConfig)` | `Skyflow` | Add a vault configuration | -| `getVaultConfig(String vaultId)` | `VaultConfig` | Retrieve a vault configuration by ID | -| `updateVaultConfig(VaultConfig)` | `Skyflow` | Replace a vault configuration (matched by `vaultId`) | -| `removeVaultConfig(String vaultId)` | `Skyflow` | Remove a vault configuration | -| `addConnectionConfig(ConnectionConfig)` | `Skyflow` | Add a connection configuration | -| `getConnectionConfig(String connectionId)` | `ConnectionConfig` | Retrieve a connection configuration by ID | -| `updateConnectionConfig(ConnectionConfig)` | `Skyflow` | Replace a connection configuration | -| `removeConnectionConfig(String connectionId)` | `Skyflow` | Remove a connection configuration | -| `updateSkyflowCredentials(Credentials)` | `Skyflow` | Replace the client-level credentials | -| `updateLogLevel(LogLevel)` | `Skyflow` | Change the log level after initialization | -| `getLogLevel()` | `LogLevel` | Return the current log level | - -All mutating methods return the `Skyflow` instance for chaining and throw `SkyflowException` on validation errors. - -# Error Handling - -The SDK uses `SkyflowException` for all errors — both client-side validation errors and server-side API errors. - -## Catching SkyflowException - -Wrap SDK calls in a `try/catch` block and catch `SkyflowException` to handle Skyflow-specific errors separately from unexpected exceptions: - -```java -import com.skyflow.errors.SkyflowException; - -try { - InsertResponse response = skyflowClient.vault().insert(insertRequest); -} catch (SkyflowException e) { - System.err.println("Skyflow error:"); - System.err.println(" HTTP code : " + e.getHttpCode()); - System.err.println(" Message : " + e.getMessage()); - System.err.println(" Request ID: " + e.getRequestId()); - System.err.println(" Details : " + e.getDetails()); -} catch (Exception e) { - System.err.println("Unexpected error: " + e.getMessage()); -} -``` - -## SkyflowException properties - -| Property | Method | Description | -|---|---|---| -| HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). | -| Message | `getMessage()` | Human-readable description of the error. | -| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). | -| gRPC code | `getGrpcCode()` | gRPC status code from the server. | -| Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. | -| Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. | - -**Validation errors** (missing table name, empty token list, etc.) are thrown before any network call: -- `httpCode` is always `400` -- `requestId` and `grpcCode` are `null` -- `details` is an empty array - -**API errors** are returned by the Skyflow server and have all fields populated from the response body and headers. - -# Logging - -The SDK provides logging with Java's built-in logging library. By default, the SDK's logging level is set to `LogLevel.ERROR`. This can be changed using the `setLogLevel(logLevel)` method, as shown below: - -Currently, the following five log levels are supported: +## Which package do I want? -- `DEBUG`**:** - When `LogLevel.DEBUG` is passed, logs at all levels will be printed (DEBUG, INFO, WARN, ERROR). -- `INFO`**:** - When `LogLevel.INFO` is passed, INFO logs for every event that occurs during SDK flow execution will be printed, along with WARN and ERROR logs. -- `WARN`**:** - When `LogLevel.WARN` is passed, only WARN and ERROR logs will be printed. -- `ERROR`**:** - When `LogLevel.ERROR` is passed, only ERROR logs will be printed. -- `OFF`**:** - `LogLevel.OFF` can be used to turn off all logging from the Skyflow Java SDK. +| Package | Artifact | README | Vault Type | Version line | +|---|---|---|---|---| +| **skyvault** | `com.skyflow:skyflow-java` | [skyvault/README.md](skyvault/README.md) | Privacy DB | 2.x | +| **flowvault** | `com.skyflow:skyflow-flowvault-java` | [flowvault/README.md](flowvault/README.md) | Flow DB | 1.x | -**Note:** The ranking of logging levels is as follows: `DEBUG` \< `INFO` \< `WARN` \< `ERROR` \< `OFF`. +`flowvault` shares auth/client setup with `skyvault` — both depend on the `common` module. -```java -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; +> **The two artifacts are versioned independently.** `flowvault` is a new SDK starting at `1.0.0`; its lower version number reflects a first release, not an older or lesser SDK than `skyvault` 2.x. Upgrade each on its own version line. -/** - * This example demonstrates how to configure the Skyflow client with custom log levels - * and authentication credentials (either token, credentials string, or other methods). - * It also shows how to configure a vault connection using specific parameters. - * - * 1. Set up credentials with a Bearer token or credentials string. - * 2. Define the Vault configuration. - * 3. Build the Skyflow client with the chosen configuration and set log level. - * 4. Example of changing the log level from ERROR (default) to INFO. - */ -public class ChangeLogLevel { - public static void main(String[] args) throws SkyflowException { - // Step 1: Set up credentials - either pass token or use credentials string - // In this case, we are using a Bearer token for authentication - Credentials credentials = new Credentials(); - credentials.setToken(""); // Replace with actual Bearer token +> Migrating from v1? See skyvault's **[Migration Guide](docs/migrate_to_v2.md)**. V1 is in maintenance mode and will reach End of Life on October 31, 2026. - // Step 2: Define the Vault configuration - // Configure the vault with necessary details like vault ID, cluster ID, and environment - VaultConfig config = new VaultConfig(); - config.setVaultId(""); // Replace with actual Vault ID (primary vault) - config.setClusterId(""); // Replace with actual Cluster ID (from vault URL) - config.setEnv(Env.PROD); // Set the environment (default is PROD) - config.setCredentials(credentials); // Set credentials for the vault (either token or credentials) +## Repository layout - // Step 3: Define additional Skyflow credentials (optional, if needed for credentials string) - // Create a JSON object to hold your Skyflow credentials - JsonObject credentialsObject = new JsonObject(); - credentialsObject.addProperty("clientId", ""); // Replace with your client ID - credentialsObject.addProperty("clientName", ""); // Replace with your client name - credentialsObject.addProperty("tokenUri", ""); // Replace with your token URI - credentialsObject.addProperty("keyId", ""); // Replace with your key ID - credentialsObject.addProperty("privateKey", ""); // Replace with your private key +The root `pom.xml` (`packaging=pom`) aggregates this Maven reactor: - // Convert the credentials object to a string format to be used for generating a Bearer Token - Credentials skyflowCredentials = new Credentials(); - skyflowCredentials.setCredentialsString(credentialsObject.toString()); // Set credentials string +- `common/` — shared client, credentials, config, and error-handling code used by both `skyvault` and `flowvault` +- `skyvault/` — the `skyflow-java` SDK ([README](skyvault/README.md)) +- `flowvault/` — the `skyflow-flowvault-java` SDK ([README](flowvault/README.md)) - // Step 4: Build the Skyflow client with the chosen configuration and log level - Skyflow skyflowClient = Skyflow.builder() - .addVaultConfig(config) // Add the Vault configuration - .addSkyflowCredentials(skyflowCredentials) // Use Skyflow credentials if no token is passed - .setLogLevel(LogLevel.INFO) // Set log level to INFO (default is ERROR) - .build(); // Build the Skyflow client +## Documentation - // Now, the Skyflow client is ready to use with the specified log level and credentials - System.out.println("Skyflow client has been successfully configured with log level: INFO."); - } -} -``` +- [skyvault API Reference](docs/api_reference.md) — full list of request builder methods, response getters, enums, and service-account utilities +- [Migrate from v1 to v2](docs/migrate_to_v2.md) -# Reporting a Vulnerability +## Reporting a Vulnerability If you discover a potential security issue in this project, please reach out to us at **security@skyflow.com**. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. diff --git a/codecov.yml b/codecov.yml index 05c58c3e..0191c11b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,52 +1,146 @@ -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 +# 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: - - src/main/java/com/skyflow/serviceaccount/** + - "**/src/main/java/com/skyflow/serviceaccount/**" - component_id: vault_data name: Vault Data paths: - - src/main/java/com/skyflow/vault/data/** + - "**/src/main/java/com/skyflow/vault/data/**" - component_id: vault_tokens name: Vault Tokens paths: - - src/main/java/com/skyflow/vault/tokens/** + - "**/src/main/java/com/skyflow/vault/tokens/**" - component_id: vault_connection name: Vault Connection paths: - - src/main/java/com/skyflow/vault/connection/** + - "**/src/main/java/com/skyflow/vault/connection/**" - component_id: vault_controller name: Vault Controller paths: - - src/main/java/com/skyflow/vault/controller/** + - "**/src/main/java/com/skyflow/vault/controller/**" - component_id: vault_detect name: Detect paths: - - src/main/java/com/skyflow/vault/detect/** + - "**/src/main/java/com/skyflow/vault/detect/**" - component_id: vault_audit name: Audit paths: - - src/main/java/com/skyflow/vault/audit/** + - "**/src/main/java/com/skyflow/vault/audit/**" - component_id: vault_bin name: BIN Lookup paths: - - src/main/java/com/skyflow/vault/bin/** + - "**/src/main/java/com/skyflow/vault/bin/**" - component_id: config name: Config paths: - - src/main/java/com/skyflow/config/** + - "**/src/main/java/com/skyflow/config/**" - component_id: utils name: Utils paths: - - src/main/java/com/skyflow/utils/** + - "**/src/main/java/com/skyflow/utils/**" - component_id: errors name: Errors paths: - - src/main/java/com/skyflow/errors/** + - "**/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/pom.xml b/common/pom.xml new file mode 100644 index 00000000..72fff275 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.skyflow + skyflow + 1.0.0 + ../pom.xml + + + common + 1.0.0 + ${project.groupId}:${project.artifactId} + + + + true + 8 + 8 + UTF-8 + + \ No newline at end of file diff --git a/common/src/main/java/com/skyflow/BaseSkyflow.java b/common/src/main/java/com/skyflow/BaseSkyflow.java new file mode 100644 index 00000000..d02dc45a --- /dev/null +++ b/common/src/main/java/com/skyflow/BaseSkyflow.java @@ -0,0 +1,214 @@ +package com.skyflow; + +import com.skyflow.config.BaseVaultConfig; +import com.skyflow.config.Credentials; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.BaseUtils; +import com.skyflow.utils.logger.LogUtil; +import com.skyflow.utils.validations.BaseValidations; + +import java.util.LinkedHashMap; +import java.util.Map; + + +abstract class BaseSkyflow, V extends BaseVaultConfig> implements ISkyflow { + protected final BaseSkyflowClientBuilder builder; + + protected BaseSkyflow(BaseSkyflowClientBuilder builder) { + this.builder = builder; + LogUtil.printInfoLog(InfoLogs.CLIENT_INITIALIZED.getLog()); + } + + protected abstract Self self(); + + @Override + public Self addVaultConfig(V vaultConfig) throws SkyflowException { + this.builder.addVaultConfigTemplate(vaultConfig); + return self(); + } + + public V getVaultConfig(String vaultId) { + return this.builder.vaultConfigMap.get(vaultId); + } + + @Override + public Self updateVaultConfig(V vaultConfig) throws SkyflowException { + this.builder.updateVaultConfigTemplate(vaultConfig); + return self(); + } + + @Override + public Self removeVaultConfig(String vaultId) throws SkyflowException { + this.builder.removeVaultConfigTemplate(vaultId); + return self(); + } + + @Override + public Self updateSkyflowCredentials(Credentials credentials) throws SkyflowException { + this.builder.addSkyflowCredentialsTemplate(credentials); + return self(); + } + + @Override + public Self setLogLevel(LogLevel logLevel) { + this.builder.setLogLevel(logLevel); + return self(); + } + + @Override + public LogLevel getLogLevel() { + return this.builder.logLevel; + } + + protected static T resolveOrThrow(Map map, String key, + ErrorLogs errorLog, ErrorMessage errorMessage) throws SkyflowException { + T value = key != null ? map.get(key) : map.values().stream().findFirst().orElse(null); + if (value == null) { + // The log line carries a %s1 placeholder for the id. Callers that resolve the single + // configured entry pass no key, so say so rather than emitting the raw placeholder. + LogUtil.printErrorLog(BaseUtils.parameterizedString( + errorLog.getLog(), key != null ? key : "not specified")); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), errorMessage.getMessage()); + } + return value; + } + + abstract static class BaseSkyflowClientBuilder { + protected final LinkedHashMap vaultConfigMap = new LinkedHashMap<>(); + protected Credentials skyflowCredentials; + protected LogLevel logLevel = LogLevel.ERROR; + + protected BaseSkyflowClientBuilder() { + } + + public BaseSkyflowClientBuilder addVaultConfig(V vaultConfig) throws SkyflowException { + addVaultConfigTemplate(vaultConfig); + return this; + } + + public BaseSkyflowClientBuilder updateVaultConfig(V vaultConfig) throws SkyflowException { + updateVaultConfigTemplate(vaultConfig); + return this; + } + + public BaseSkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { + removeVaultConfigTemplate(vaultId); + return this; + } + + public BaseSkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throws SkyflowException { + addSkyflowCredentialsTemplate(credentials); + return this; + } + + protected BaseSkyflowClientBuilder setLogLevel(LogLevel logLevel) { + this.logLevel = logLevel == null ? LogLevel.ERROR : logLevel; + LogUtil.setupLogger(this.logLevel); + LogUtil.printInfoLog(BaseUtils.parameterizedString( + InfoLogs.CURRENT_LOG_LEVEL.getLog(), String.valueOf(this.logLevel) + )); + return this; + } + + protected final void addVaultConfigTemplate(V vaultConfig) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.VALIDATING_VAULT_CONFIG.getLog()); + validateVaultConfig(vaultConfig); + V vaultConfigCopy = cloneVaultConfig(vaultConfig); + String vaultId = extractVaultId(vaultConfigCopy); + if (hasVaultClient(vaultId)) { + LogUtil.printErrorLog(BaseUtils.parameterizedString( + ErrorLogs.VAULT_CONFIG_EXISTS.getLog(), vaultId + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.VaultIdAlreadyInConfigList.getMessage()); + } + onVaultConfigAdded(vaultConfigCopy); + this.vaultConfigMap.put(vaultId, vaultConfigCopy); + } + + protected final void updateVaultConfigTemplate(V vaultConfig) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.VALIDATING_VAULT_CONFIG.getLog()); + validateVaultConfig(vaultConfig); + String vaultId = extractVaultId(vaultConfig); + if (!hasVaultClient(vaultId)) { + LogUtil.printErrorLog(BaseUtils.parameterizedString( + ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog(), vaultId + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); + } + V previousConfig = this.vaultConfigMap.get(vaultId); + V merged = mergeVaultConfig(vaultConfig, cloneVaultConfig(previousConfig)); + onVaultConfigUpdated(merged); + this.vaultConfigMap.put(vaultId, merged); + } + + protected final void removeVaultConfigTemplate(String vaultId) throws SkyflowException { + if (!hasVaultClient(vaultId)) { + LogUtil.printErrorLog(BaseUtils.parameterizedString(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog(), vaultId)); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); + } + onVaultConfigRemoved(vaultId); + this.vaultConfigMap.remove(vaultId); + } + + protected final void addSkyflowCredentialsTemplate(Credentials credentials) throws SkyflowException { + BaseValidations.validateCredentials(credentials); + Credentials credentialsCopy; + try { + credentialsCopy = (Credentials) credentials.clone(); + } catch (CloneNotSupportedException e) { + throw new SkyflowException(e.getMessage(), e); + } + onCredentialsUpdated(credentialsCopy); + this.skyflowCredentials = credentialsCopy; + } + + protected abstract void validateVaultConfig(V vaultConfig) throws SkyflowException; + + protected abstract boolean hasVaultClient(String vaultId); + + @SuppressWarnings("unchecked") + protected final V cloneVaultConfig(V vaultConfig) throws SkyflowException { + try { + return (V) vaultConfig.clone(); + } catch (CloneNotSupportedException e) { + throw new SkyflowException(e.getMessage(), e); + } + } + + protected final String extractVaultId(V vaultConfig) { + return vaultConfig.getVaultId(); + } + + protected final V mergeVaultConfig(V incoming, V existing) throws SkyflowException { + if (incoming.getEnv() != null) { + existing.setEnv(incoming.getEnv()); + } + if (incoming.getClusterId() != null) { + existing.setClusterId(incoming.getClusterId()); + } + if (incoming.getCredentials() != null) { + try { + existing.setCredentials((Credentials) incoming.getCredentials().clone()); + } catch (CloneNotSupportedException e) { + throw new SkyflowException(e.getMessage(), e); + } + } + return existing; + } + + protected abstract void onVaultConfigAdded(V vaultConfig) throws SkyflowException; + + protected abstract void onVaultConfigUpdated(V updatedConfig) throws SkyflowException; + + protected abstract void onVaultConfigRemoved(String vaultId) throws SkyflowException; + + protected abstract void onCredentialsUpdated(Credentials credentials) throws SkyflowException; + } + +} diff --git a/common/src/main/java/com/skyflow/BaseVaultClient.java b/common/src/main/java/com/skyflow/BaseVaultClient.java new file mode 100644 index 00000000..3d5b0d32 --- /dev/null +++ b/common/src/main/java/com/skyflow/BaseVaultClient.java @@ -0,0 +1,114 @@ +package com.skyflow; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.skyflow.config.BaseCredentials; +import com.skyflow.config.BaseVaultConfig; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.logs.InfoLogs; +import com.skyflow.serviceaccount.util.Token; +import com.skyflow.utils.BaseConstants; +import com.skyflow.utils.BaseUtils; +import com.skyflow.utils.logger.LogUtil; +import com.skyflow.utils.validations.BaseValidations; +import io.github.cdimascio.dotenv.Dotenv; +import io.github.cdimascio.dotenv.DotenvException; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +class BaseVaultClient { + protected V vaultConfig; + protected OkHttpClient sharedHttpClient; + protected String currentVaultURL; + protected BaseCredentials commonCredentials; + protected BaseCredentials finalCredentials; + protected String token; + protected String apiKey; + + protected BaseVaultClient(V vaultConfig, BaseCredentials credentials) { + this.vaultConfig = vaultConfig; + this.commonCredentials = credentials; + } + + protected V getVaultConfig() { + return vaultConfig; + } + + protected OkHttpClient buildSharedHttpClient(Supplier tokenSupplier) { + return new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) + .addInterceptor(chain -> { + Request requestWithAuth = chain.request().newBuilder() + .header("Authorization", "Bearer " + tokenSupplier.get()) + .build(); + return chain.proceed(requestWithAuth); + }) + .build(); + } + + protected synchronized void prioritiseCredentials(BaseCredentials vaultSpecificCredentials) throws SkyflowException { + try { + BaseCredentials original = this.finalCredentials; + if (vaultSpecificCredentials != null) { + this.finalCredentials = vaultSpecificCredentials; + } else if (this.commonCredentials != null) { + this.finalCredentials = this.commonCredentials; + } else { + String sysCredentials = System.getenv(BaseConstants.ENV_CREDENTIALS_KEY_NAME); + if (sysCredentials == null) { + Dotenv dotenv = Dotenv.load(); + sysCredentials = dotenv.get(BaseConstants.ENV_CREDENTIALS_KEY_NAME); + } + if (sysCredentials == null) { + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentials.getMessage()); + } else { + this.finalCredentials = new BaseCredentials(); + this.finalCredentials.setCredentialsString(sysCredentials); + } + } + if (original != null && !original.equals(this.finalCredentials)) { + token = null; + apiKey = null; + } + } catch (DotenvException e) { + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentials.getMessage()); + } catch (SkyflowException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected synchronized void setBearerToken(BaseCredentials vaultSpecificCredentials) throws SkyflowException { + prioritiseCredentials(vaultSpecificCredentials); + BaseValidations.validateCredentials(this.finalCredentials); + if (this.finalCredentials.getApiKey() != null) { + LogUtil.printInfoLog(InfoLogs.USE_API_KEY.getLog()); + token = this.finalCredentials.getApiKey(); + } else if (token == null || token.trim().isEmpty()) { + token = BaseUtils.generateBearerToken(this.finalCredentials); + } else if (Token.isExpired(token)) { + LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_EXPIRED.getLog()); + token = BaseUtils.generateBearerToken(this.finalCredentials); + } else { + LogUtil.printInfoLog(InfoLogs.REUSE_BEARER_TOKEN.getLog()); + } + } + + protected static SkyflowException wrapApiException(int statusCode, Throwable cause, + Map> headers, + Object responseBody, ErrorLogs errorLog) { + LogUtil.printErrorLog(errorLog.getLog()); + Gson gson = new GsonBuilder().serializeNulls().create(); + return new SkyflowException(statusCode, cause, headers, gson.toJson(responseBody)); + } +} diff --git a/common/src/main/java/com/skyflow/ISkyflow.java b/common/src/main/java/com/skyflow/ISkyflow.java new file mode 100644 index 00000000..1c29b780 --- /dev/null +++ b/common/src/main/java/com/skyflow/ISkyflow.java @@ -0,0 +1,20 @@ +package com.skyflow; + +import com.skyflow.config.BaseCredentials; +import com.skyflow.config.BaseVaultConfig; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; + +public interface ISkyflow, V extends BaseVaultConfig, C extends BaseCredentials> { + Self addVaultConfig(V vaultConfig) throws SkyflowException; + + Self updateVaultConfig(V vaultConfig) throws SkyflowException; + + Self removeVaultConfig(String vaultId) throws SkyflowException; + + Self updateSkyflowCredentials(C credentials) throws SkyflowException; + + Self setLogLevel(LogLevel logLevel); + + LogLevel getLogLevel(); +} diff --git a/src/main/java/com/skyflow/config/Credentials.java b/common/src/main/java/com/skyflow/config/BaseCredentials.java similarity index 71% rename from src/main/java/com/skyflow/config/Credentials.java rename to common/src/main/java/com/skyflow/config/BaseCredentials.java index c2594ef6..48ab70dc 100644 --- a/src/main/java/com/skyflow/config/Credentials.java +++ b/common/src/main/java/com/skyflow/config/BaseCredentials.java @@ -1,20 +1,21 @@ package com.skyflow.config; import java.util.ArrayList; +import java.util.HashMap; import java.util.Map; -public class Credentials { +public class BaseCredentials implements Cloneable { private String path; private ArrayList roles; - private Object context; private String credentialsString; private String token; private String apiKey; + private Object context; - public Credentials() { + public BaseCredentials() { this.path = null; - this.context = null; this.credentialsString = null; + this.context = null; } public String getPath() { @@ -33,18 +34,6 @@ public void setRoles(ArrayList roles) { this.roles = roles; } - public Object getContext() { - return context; - } - - public void setContext(String context) { - this.context = context; - } - - public void setContext(Map context) { - this.context = context; - } - public String getCredentialsString() { return credentialsString; } @@ -68,4 +57,28 @@ public String getApiKey() { public void setApiKey(String apiKey) { this.apiKey = apiKey; } + public Object getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public void setContext(Map context) { + this.context = context; + } + + @Override + @SuppressWarnings("unchecked") + public Object clone() throws CloneNotSupportedException { + BaseCredentials copy = (BaseCredentials) super.clone(); + if (this.roles != null) { + copy.roles = new ArrayList<>(this.roles); + } + if (this.context instanceof Map) { + copy.context = new HashMap<>((Map) this.context); + } + return copy; + } } diff --git a/src/main/java/com/skyflow/config/VaultConfig.java b/common/src/main/java/com/skyflow/config/BaseVaultConfig.java similarity index 71% rename from src/main/java/com/skyflow/config/VaultConfig.java rename to common/src/main/java/com/skyflow/config/BaseVaultConfig.java index 4f61af2a..fb1a2a6b 100644 --- a/src/main/java/com/skyflow/config/VaultConfig.java +++ b/common/src/main/java/com/skyflow/config/BaseVaultConfig.java @@ -2,13 +2,13 @@ import com.skyflow.enums.Env; -public class VaultConfig { +public class BaseVaultConfig implements Cloneable { private String vaultId; private String clusterId; private Env env; private Credentials credentials; - public VaultConfig() { + public BaseVaultConfig() { this.vaultId = null; this.clusterId = null; this.env = Env.PROD; @@ -46,4 +46,14 @@ public Credentials getCredentials() { public void setCredentials(Credentials credentials) { this.credentials = credentials; } + + @Override + public Object clone() throws CloneNotSupportedException { + BaseVaultConfig cloned = (BaseVaultConfig) super.clone(); + if (this.credentials != null) { + cloned.credentials = (Credentials) this.credentials.clone(); + } + return cloned; + } + } diff --git a/common/src/main/java/com/skyflow/config/Credentials.java b/common/src/main/java/com/skyflow/config/Credentials.java new file mode 100644 index 00000000..b0d6b888 --- /dev/null +++ b/common/src/main/java/com/skyflow/config/Credentials.java @@ -0,0 +1,7 @@ +package com.skyflow.config; + +public class Credentials extends BaseCredentials { + public Credentials() { + super(); + } +} diff --git a/src/main/java/com/skyflow/enums/Env.java b/common/src/main/java/com/skyflow/enums/Env.java similarity index 100% rename from src/main/java/com/skyflow/enums/Env.java rename to common/src/main/java/com/skyflow/enums/Env.java diff --git a/src/main/java/com/skyflow/enums/LogLevel.java b/common/src/main/java/com/skyflow/enums/LogLevel.java similarity index 100% rename from src/main/java/com/skyflow/enums/LogLevel.java rename to common/src/main/java/com/skyflow/enums/LogLevel.java diff --git a/src/main/java/com/skyflow/errors/ErrorCode.java b/common/src/main/java/com/skyflow/errors/ErrorCode.java similarity index 100% rename from src/main/java/com/skyflow/errors/ErrorCode.java rename to common/src/main/java/com/skyflow/errors/ErrorCode.java diff --git a/common/src/main/java/com/skyflow/errors/ErrorMessage.java b/common/src/main/java/com/skyflow/errors/ErrorMessage.java new file mode 100644 index 00000000..2f52addf --- /dev/null +++ b/common/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -0,0 +1,223 @@ +package com.skyflow.errors; + +import com.skyflow.utils.SdkVersion; + +public enum ErrorMessage { + // Client initialization + VaultIdAlreadyInConfigList("%s0 Validation error. VaultId is present in an existing config. Specify a new vaultId in config."), + VaultIdNotInConfigList("%s0 Validation error. VaultId is missing from the config. Specify the vaultIds from configs."), + OnlySingleVaultConfigAllowed("%s0 Validation error. A vault config already exists. Cannot add another vault config."), + ConnectionIdAlreadyInConfigList("%s0 Validation error. ConnectionId is present in an existing config. Specify a connectionId in config."), + ConnectionIdNotInConfigList("%s0 Validation error. ConnectionId is missing from the config. Specify the connectionIds from configs."), + EmptyCredentials("%s0 Validation error. Invalid credentials. Credentials must not be empty."), + TableSpecifiedInRequestAndRecordObject("%s0 Validation error. Table name cannot be specified at both the request and record levels. Please specify the table name in only one place."), + UpsertTableRequestAtRecordLevel("%s0 Validation error. Table name should be present at each record level when upsert is present at record level."), + UpsertTableRequestAtRequestLevel("%s0 Validation error. Upsert should be present at each record level when table name is present at record level."), + TableNotSpecifiedInRequestAndRecordObject("%s0 Validation error. Table name is missing. Table name should be specified at one place either at the request level or record level. Please specify the table name at one place."), + // Vault config + InvalidVaultId("%s0 Initialization failed. Invalid vault ID. Specify a valid vault ID."), + EmptyVaultId("%s0 Initialization failed. Invalid vault ID. Vault ID must not be empty."), + InvalidClusterId("%s0 Initialization failed. Invalid cluster ID. Specify cluster ID."), + EmptyClusterId("%s0 Initialization failed. Invalid cluster ID. Specify a valid cluster ID."), + EmptyVaultUrl("%s0 Initialization failed. Vault URL is empty. Specify a valid vault URL."), + InvalidVaultUrlFormat("%s0 Initialization failed. Vault URL must start with 'https://'."), + EitherVaultUrlOrClusterIdRequired("%s0 Initialization failed. Specify either 'clusterId' or 'vaultURL'."), + + // Connection config + InvalidConnectionId("%s0 Initialization failed. Invalid connection ID. Specify a valid connection ID."), + EmptyConnectionId("%s0 Initialization failed. Invalid connection ID. Connection ID must not be empty."), + InvalidConnectionUrl("%s0 Initialization failed. Invalid connection URL. Specify a valid connection URL."), + EmptyConnectionUrl("%s0 Initialization failed. Invalid connection URL. Connection URL must not be empty."), + InvalidConnectionUrlFormat("%s0 Initialization failed. Connection URL is not a valid URL. Specify a valid connection URL."), + + // Credentials + MultipleTokenGenerationMeansPassed("%s0 Initialization failed. Invalid credentials. Specify only one from 'path', 'credentialsString', 'token' or 'apiKey'."), + NoTokenGenerationMeansPassed("%s0 Initialization failed. Invalid credentials. Specify any one from 'path', 'credentialsString', 'token' or 'apiKey'."), + EmptyCredentialFilePath("%s0 Initialization failed. Invalid credentials. Credentials file path must not be empty."), + EmptyCredentialsString("%s0 Initialization failed. Invalid credentials. Credentials string must not be empty."), + EmptyToken("%s0 Initialization failed. Invalid credentials. Token must not be empty."), + EmptyApikey("%s0 Initialization failed. Invalid credentials. Api key must not be empty."), + InvalidApikey("%s0 Initialization failed. Invalid credentials. Specify valid api key."), + EmptyRoles("%s0 Initialization failed. Invalid roles. Specify at least one role."), + EmptyRoleInRoles("%s0 Initialization failed. Invalid role. Specify a valid role."), + EmptyContext("%s0 Initialization failed. Invalid context. Specify a valid context."), + InvalidContextType("%s0 Initialization failed. Invalid context type. Specify context as a String or Map."), + InvalidContextMapKey("%s0 Initialization failed. Invalid key '%s1' in context map. Keys must contain only alphanumeric characters and underscores."), + + // Bearer token generation + FileNotFound("%s0 Initialization failed. Credential file not found at %s1. Verify the file path."), + FileInvalidJson("%s0 Initialization failed. File at %s1 is not in valid JSON format. Verify the file contents."), + CredentialsStringInvalidJson("%s0 Initialization failed. Credentials string is not in valid JSON format. Verify the credentials string contents."), + InvalidCredentials("%s0 Initialization failed. Invalid credentials provided. Specify valid credentials."), + MissingPrivateKey("%s0 Initialization failed. Unable to read private key in credentials. Verify your private key."), + MissingClientId("%s0 Initialization failed. Unable to read client ID in credentials. Verify your client ID."), + MissingKeyId("%s0 Initialization failed. Unable to read key ID in credentials. Verify your key ID."), + MissingTokenUri("%s0 Initialization failed. Unable to read token URI in credentials. Verify your token URI."), + InvalidTokenUri("%s0 Initialization failed. Token URI in not a valid URL in credentials. Verify your token URI."), + JwtInvalidFormat("%s0 Initialization failed. Invalid private key format. Verify your credentials."), + InvalidAlgorithm("%s0 Initialization failed. Invalid algorithm to parse private key. Specify valid algorithm."), + InvalidKeySpec("%s0 Initialization failed. Unable to parse RSA private key. Verify your credentials."), + JwtDecodeError("%s0 Validation error. Invalid access token. Verify your credentials."), + MissingAccessToken("%s0 Validation error. Access token not present in the response from bearer token generation. Verify your credentials."), + MissingTokenType("%s0 Validation error. Token type not present in the response from bearer token generation. Verify your credentials."), + BearerTokenExpired("%s0 Validation error. Bearer token is invalid or expired. Please provide a valid bearer token."), + + // Insert + InsertRequestNull("%s0 Validation error. InsertRequest object is null. Specify a valid InsertRequest object."), + TableKeyError("%s0 Validation error. 'table' key is missing from the payload. Specify a 'table' key."), + EmptyTable("%s0 Validation error. 'table' can't be empty. Specify a table."), + ValuesKeyError("%s0 Validation error. 'values' key is missing from the payload. Specify a 'values' key."), + EmptyRecords("%s0 Validation error. 'records' can't be empty. Specify records."), + EmptyKeyInRecords("%s0 Validation error. Invalid key in data in records. Specify a valid key."), + EmptyValueInRecords("%s0 Validation error. Invalid value in records. Specify a valid value."), + RecordsKeyError("%s0 Validation error. 'records' key is missing from the payload. Specify a 'records' key."), + EmptyValues("%s0 Validation error. 'values' can't be empty. Specify values."), + EmptyKeyInValues("%s0 Validation error. Invalid key in values. Specify a valid key."), + EmptyValueInValues("%s0 Validation error. Invalid value in values. Specify a valid value."), + TokensKeyError("%s0 Validation error. 'tokens' key is missing from the payload. Specify a 'tokens' key."), + EmptyTokens("%s0 Validation error. The 'tokens' field is empty. Specify tokens for one or more fields."), + EmptyKeyInTokens("%s0 Validation error. Invalid key tokens. Specify a valid key."), + EmptyValueInTokens("%s0 Validation error. Invalid value in tokens. Specify a valid value."), + EmptyUpsert("%s0 Validation error. 'upsert' key can't be empty. Specify an upsert column."), + InvalidUpsertUpdateType("%s0 Validation error. Invalid upsert updateType. Specify either 'UPDATE' or 'REPLACE'."), + EmptyUpsertValues("%s0 Validation error. Upsert column values can't be empty. Specify at least one upsert column."), + HomogenousNotSupportedWithUpsert("%s0 Validation error. 'homogenous' is not supported with 'upsert'. Specify either 'homogenous' or 'upsert'."), + TokensPassedForTokenModeDisable("%s0 Validation error. 'tokenMode' wasn't specified. Set 'tokenMode' to 'ENABLE' to insert tokens."), + NoTokensWithTokenMode("%s0 Validation error. Tokens weren't specified for records while 'tokenMode' was %s1. Specify tokens."), + MismatchOfFieldsAndTokens("%s0 Validation error. 'fields' and 'tokens' have different columns names. Verify that 'fields' and 'tokens' columns match."), + InsufficientTokensPassedForTokenModeEnableStrict("%s0 Validation error. 'tokenMode' is set to 'ENABLE_STRICT', but some fields are missing tokens. Specify tokens for all fields."), + BatchInsertPartialSuccess("%s0 Insert operation completed with partial success."), + BatchInsertFailure("%s0 Insert operation failed."), + RecordSizeExceedError("%s0 Maximum number of records exceeded. The limit is 10000."), + + // Detokenize + InvalidDetokenizeData("%s0 Validation error. Invalid detokenize data. Specify valid detokenize data."), + EmptyDetokenizeData("%s0 Validation error. Invalid data tokens. Specify at least one data token."), + EmptyTokenInDetokenizeData("%s0 Validation error. Invalid data tokens. Specify a valid data token."), + TokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), + + // Delete Tokens + DeleteTokensRequestNull("%s0 Validation error. DeleteTokensRequest object is null. Specify a valid DeleteTokensRequest object."), + EmptyDeleteTokensData("%s0 Validation error. Tokens list is empty. Specify at least one token to delete."), + EmptyTokenInDeleteTokensData("%s0 Validation error. Invalid token in delete tokens request. Specify a valid token."), + DeleteTokensSizeExceedError("%s0 Maximum number of tokens exceeded. The limit is 10000."), + + // Get + IdsKeyError("%s0 Validation error. 'ids' key is missing from the payload. Specify an 'ids' key."), + EmptyIds("%s0 Validation error. 'ids' can't be empty. Specify at least one id."), + EmptyIdInIds("%s0 Validation error. Invalid id in 'ids'. Specify a valid id."), + EmptyFields("%s0 Validation error. Fields are empty in get payload. Specify at least one field."), + EmptyFieldInFields("%s0 Validation error. Invalid field in 'fields'. Specify a valid field."), + RedactionKeyError("%s0 Validation error. 'redaction' key is missing from the payload. Specify a 'redaction' key."), + RedactionWithTokensNotSupported("%s0 Validation error. 'redaction' can't be used when 'returnTokens' is specified. Remove 'redaction' from payload if 'returnTokens' is specified."), + TokensGetColumnNotSupported("%s0 Validation error. Column name and/or column values can't be used when 'returnTokens' is specified. Remove unique column values or 'returnTokens' from the payload."), + EmptyOffset("%s0 Validation error. 'offset' can't be empty. Specify an offset."), + EmptyLimit("%s0 Validation error. 'limit' can't be empty. Specify a limit."), + UniqueColumnOrIdsKeyError("%s0 Validation error. 'ids' or 'columnName' key is missing from the payload. Specify the ids or unique 'columnName' in payload."), + BothIdsAndColumnDetailsSpecified("%s0 Validation error. Both Skyflow IDs and column details can't be specified. Either specify Skyflow IDs or unique column details."), + ColumnNameKeyError("%s0 Validation error. 'columnName' isn't specified whereas 'columnValues' are specified. Either add 'columnName' or remove 'columnValues'."), + EmptyColumnName("%s0 Validation error. 'columnName' can't be empty. Specify a column name."), + ColumnValuesKeyErrorGet("%s0 Validation error. 'columnValues' aren't specified whereas 'columnName' is specified. Either add 'columnValues' or remove 'columnName'."), + EmptyColumnValues("%s0 Validation error. 'columnValues' can't be empty. Specify at least one column value"), + EmptyValueInColumnValues("%s0 Validation error. Invalid value in column values. Specify a valid column value."), + IdsOrUniqueValuesKeyError("%s0 Validation error. 'ids' or 'uniqueValues' key is missing from the payload. Specify ids or uniqueValues in payload."), + BothIdsAndUniqueValuesSpecified("%s0 Validation error. Both Skyflow IDs and unique values can't be specified. Either specify Skyflow IDs or unique values."), + EmptyUniqueValues("%s0 Validation error. 'uniqueValues' can't be empty. Specify at least one unique value."), + EmptyUniqueValueInUniqueValues("%s0 Validation error. Invalid unique value in 'uniqueValues'. Specify a valid unique value."), + NullColumnRedactions("%s0 Validation error. Column redaction object can not be null. Specify a valid column redaction object."), + NullColumnNameInColumnRedaction("%s0 Validation error. Column name can not be null or empty in column redaction. Specify a valid column name."), + NullRedactionInColumnRedaction("%s0 Validation error. Redaction can not be null or empty in column redaction. Specify a valid redaction."), + BothSingleTableFieldsAndRecordsSpecified("%s0 Validation error. Both single-table lookup fields ('table', 'ids', 'fields', 'uniqueValues', 'columnRedactions') and 'records' can't be specified. Either specify single-table fields or 'records'."), + NullGetRecordRequest("%s0 Validation error. Record in 'records' is null. Specify a valid record."), + + TokenKeyError("%s0 Validation error. 'token' key is missing from the payload. Specify a 'token' key."), + PartialSuccess("%s0 Validation error. Check 'SkyflowError.data' for details."), + + // Update + DataKeyError("%s0 Validation error. 'data' key is missing from the payload. Specify a 'data' key."), + EmptyData("%s0 Validation error. 'data' can't be empty. Specify data."), + SkyflowIdKeyError("%s0 Validation error. 'skyflow_id' is missing from the data payload. Specify a 'skyflow_id'."), + InvalidSkyflowIdType("%s0 Validation error. Invalid type for 'skyflow_id' in data payload. Specify 'skyflow_id' as a string."), + EmptySkyflowId("%s0 Validation error. 'skyflow_id' can't be empty. Specify a skyflow id."), + + // Query + QueryKeyError("%s0 Validation error. 'query' key is missing from the payload. Specify a 'query' key."), + EmptyQuery("%s0 Validation error. 'query' can't be empty. Specify a query"), + + // Tokenize + ColumnValuesKeyErrorTokenize("%s0 Validation error. 'columnValues' key is missing from the payload. Specify a 'columnValues' key."), + EmptyColumnGroupInColumnValue("%s0 Validation error. Invalid column group in column value. Specify a valid column group."), + TokenizeRequestNull("%s0 Validation error. TokenizeRequest object is null. Specify a valid TokenizeRequest object."), + EmptyTokenizeData("%s0 Validation error. Tokenize data is empty. Specify at least one tokenize record."), + TokenizeRecordNull("%s0 Validation error. TokenizeRecord in the list is null. Specify a valid TokenizeRecord object."), + EmptyValueInTokenizeRecord("%s0 Validation error. Value in TokenizeRecord is null or empty. Specify a valid value."), + EmptyTokenGroupNamesInTokenizeRecord("%s0 Validation error. TokenGroupNames in TokenizeRecord is null or empty. Specify at least one token group name."), + EmptyTokenGroupNameInTokenizeRecord("%s0 Validation error. Token group name in TokenizeRecord is null or empty. Specify a valid token group name."), + TokenizeDataSizeExceedError("%s0 Maximum number of tokenize records exceeded. The limit is 10000."), + 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."), + EmptyRequestHeaders("%s0 Validation error. Request headers are empty. Specify valid request headers."), + InvalidPathParams("%s0 Validation error. Path parameters aren't valid. Specify valid path parameters."), + EmptyPathParams("%s0 Validation error. Path parameters are empty. Specify valid path parameters."), + InvalidQueryParams("%s0 Validation error. Query parameters aren't valid. Specify valid query parameters."), + EmptyQueryParams("%s0 Validation error. Query parameters are empty. Specify valid query parameters."), + InvalidRequestBody("%s0 Validation error. Invalid request body. Specify the request body as an object."), + EmptyRequestBody("%s0 Validation error. Request body can't be empty. Specify a valid request body."), + + // File upload + ColumnNameKeyErrorFileUpload("%s0 Validation error. columnName is missing from the payload. Specify a columnName key."), + MissingFileSourceInUploadFileRequest("%s0 Validation error. Provide exactly one of filePath, base64, or fileObject."), + FileNameMustBeProvidedWithFileObject("%s0 Validation error. fileName must be provided when using fileObject."), + InvalidFileObject("%s0 Validation error. Invalid file object in file upload request. Specify a valid file object."), + InvalidBase64("%s0 Validation error. Invalid base64 string in file upload request. Specify a valid base64 string."), + + // detect + InvalidTextInDeIdentify("%s0 Validation error. The text field is required and must be a non-empty string. Specify a valid text."), + InvalidTextInReIdentify("%s0 Validation error. The text field is required and must be a non-empty string. Specify a valid text."), + + //Detect Files + InvalidNullFileInDeIdentifyFile("%s0 Validation error. The file field is required and must not be null. Specify a valid file object."), + InvalidFilePath("%s0 Validation error. The file path is invalid. Specify a valid file path."), + BothFileAndFilePathProvided("%s0 Validation error. Both file and filePath are provided. Specify either file object or filePath, not both."), + FileNotFoundToDeidentify("%s0 Validation error. The file to deidentify was not found at the specified path. Verify the file path and try again."), + FileNotReadableToDeidentify("%s0 Validation error. The file to deidentify is not readable. Check the file permissions and try again."), + InvalidPixelDensityToDeidentifyFile("%s0 Validation error. Should be a positive integer. Specify a valid pixel density."), + InvalidMaxResolution("%s0 Validation error. Should be a positive integer. Specify a valid max resolution."), + OutputDirectoryNotFound("%s0 Validation error. The output directory for deidentified files was not found at the specified path. Verify the output directory path and try again."), + InvalidPermission("%s0 Validation error. The output directory for deidentified files is not writable. Check the directory permissions and try again."), + InvalidWaitTime("%s0 Validation error. The wait time for deidentify file operation should be a positive integer. Specify a valid wait time."), + WaitTimeExceedsLimit("%s0 Validation error. The wait time for deidentify file operation exceeds the maximum limit of 64 seconds. Specify a wait time less than or equal to 60 seconds."), + InvalidOrEmptyRunId("%s0 Validation error. The run ID is invalid or empty. Specify a valid run ID."), + FailedToEncodeFile("%s0 Validation error. Failed to encode the file. Ensure the file is in a supported format and try again."), + FailedToDecodeFileFromResponse("%s0 Failed to decode the file from the response. Ensure the response is valid and try again."), + EmptyFileAndFilePathInDeIdentifyFile("%s0 Validation error. Both file and filePath are empty. Specify either file object or filePath, not both."), + VaultTokenFormatIsNotAllowedForFiles("%s0 Validation error. Vault token format is not allowed for deidentify file request."), + PollingForResultsFailed("%s0 API error. Polling for results failed. Unable to retrieve the deidentified file"), + FailedToSaveProcessedFile("%s0 Validation error. Failed to save the processed file. Ensure the output directory is valid and writable."), + InvalidAudioFileType("%s0 Validation error. The file type is not supported. Specify a valid file type mp3 or wav."), + // Generic + ErrorOccurred("%s0 API error. Error occurred."), + + DetokenizeRequestNull("%s0 Validation error. DetokenizeRequest object is null. Specify a valid DetokenizeRequest object."), + + NullTokenGroupRedactions("%s0 Validation error. TokenGroupRedaction in the list is null. Specify a valid TokenGroupRedactions object."), + + NullRedactionInTokenGroup("%s0 Validation error. Redaction in TokenGroupRedactions is null or empty. Specify a valid redaction."), + + NullTokenGroupNameInTokenGroup("%s0 Validation error. TokenGroupName in TokenGroupRedactions is null or empty. Specify a valid tokenGroupName."), + InvalidRecord("%s0 Validation error. InsertRecord object in the list is invalid. Specify a valid InsertRecord object."), + ; + + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message.replace("%s0", SdkVersion.getSdkPrefix()); + } +} diff --git a/src/main/java/com/skyflow/errors/HttpStatus.java b/common/src/main/java/com/skyflow/errors/HttpStatus.java similarity index 100% rename from src/main/java/com/skyflow/errors/HttpStatus.java rename to common/src/main/java/com/skyflow/errors/HttpStatus.java diff --git a/src/main/java/com/skyflow/errors/SkyflowException.java b/common/src/main/java/com/skyflow/errors/SkyflowException.java similarity index 95% rename from src/main/java/com/skyflow/errors/SkyflowException.java rename to common/src/main/java/com/skyflow/errors/SkyflowException.java index 6fedf9c3..2b043f92 100644 --- a/src/main/java/com/skyflow/errors/SkyflowException.java +++ b/common/src/main/java/com/skyflow/errors/SkyflowException.java @@ -4,7 +4,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.skyflow.utils.Constants; +import com.skyflow.utils.BaseConstants; import java.util.List; import java.util.Map; @@ -27,7 +27,7 @@ *

Typical error-handling pattern: *

{@code
  * try {
- *     InsertResponse response = vault.insert(request);
+ *     response = vault.insert(request);
  * } catch (SkyflowException e) {
  *     System.err.println("HTTP " + e.getHttpCode() + " — " + e.getMessage());
  *     if (e.getRequestId() != null) {
@@ -113,7 +113,7 @@ public String getRequestId() {
     }
 
     private void setRequestId(Map> responseHeaders) {
-        List ids = responseHeaders.get(Constants.REQUEST_ID_HEADER_KEY);
+        List ids = responseHeaders.get(BaseConstants.REQUEST_ID_HEADER_KEY);
         this.requestId = ids == null ? null : ids.get(0);
     }
 
@@ -134,10 +134,11 @@ private void setHttpStatus() {
 
     /**
      * Returns the HTTP status code (e.g. 400, 404, 500).
-     * Defaults to 400 when the server returned a non-positive code.
+     * Defaults to 400 when the server returned a non-positive code, and 0 when the
+     * exception carries no HTTP code at all (e.g. it wraps a local failure).
      */
     public int getHttpCode() {
-        return httpCode;
+        return httpCode == null ? 0 : httpCode;
     }
 
     /**
@@ -151,7 +152,7 @@ public JsonArray getDetails() {
 
     private void setDetails(Map> responseHeaders) {
         JsonElement detailsElement = ((JsonObject) responseBody.get("error")).get("details");
-        List errorFromClientHeader = responseHeaders.get(Constants.ERROR_FROM_CLIENT_HEADER_KEY);
+        List errorFromClientHeader = responseHeaders.get(BaseConstants.ERROR_FROM_CLIENT_HEADER_KEY);
         if (detailsElement != null) {
             this.details = detailsElement.getAsJsonArray();
         }
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/ApiClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/ApiClient.java
new file mode 100644
index 00000000..a1dd3aae
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/ApiClient.java
@@ -0,0 +1,29 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest;
+
+import com.skyflow.generated.auth.rest.core.ClientOptions;
+import com.skyflow.generated.auth.rest.core.Suppliers;
+import com.skyflow.generated.auth.rest.resources.authentication.AuthenticationClient;
+
+import java.util.function.Supplier;
+
+public class ApiClient {
+    protected final ClientOptions clientOptions;
+
+    protected final Supplier authenticationClient;
+
+    public ApiClient(ClientOptions clientOptions) {
+        this.clientOptions = clientOptions;
+        this.authenticationClient = Suppliers.memoize(() -> new AuthenticationClient(clientOptions));
+    }
+
+    public AuthenticationClient authentication() {
+        return this.authenticationClient.get();
+    }
+
+    public static ApiClientBuilder builder() {
+        return new ApiClientBuilder();
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/ApiClientBuilder.java b/common/src/main/java/com/skyflow/generated/auth/rest/ApiClientBuilder.java
new file mode 100644
index 00000000..aed3ed24
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/ApiClientBuilder.java
@@ -0,0 +1,67 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest;
+
+import com.skyflow.generated.auth.rest.core.ClientOptions;
+import com.skyflow.generated.auth.rest.core.Environment;
+import okhttp3.OkHttpClient;
+
+public final class ApiClientBuilder {
+    private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder();
+
+    private String token = null;
+
+    private Environment environment = Environment.PRODUCTION;
+
+    /**
+     * Sets token
+     */
+    public ApiClientBuilder token(String token) {
+        this.token = token;
+        return this;
+    }
+
+    public ApiClientBuilder environment(Environment environment) {
+        this.environment = environment;
+        return this;
+    }
+
+    public ApiClientBuilder url(String url) {
+        this.environment = Environment.custom(url);
+        return this;
+    }
+
+    /**
+     * Sets the timeout (in seconds) for the client. Defaults to 60 seconds.
+     */
+    public ApiClientBuilder timeout(int timeout) {
+        this.clientOptionsBuilder.timeout(timeout);
+        return this;
+    }
+
+    /**
+     * Sets the maximum number of retries for the client. Defaults to 2 retries.
+     */
+    public ApiClientBuilder maxRetries(int maxRetries) {
+        this.clientOptionsBuilder.maxRetries(maxRetries);
+        return this;
+    }
+
+    /**
+     * Sets the underlying OkHttp client
+     */
+    public ApiClientBuilder httpClient(OkHttpClient httpClient) {
+        this.clientOptionsBuilder.httpClient(httpClient);
+        return this;
+    }
+
+    public ApiClient build() {
+        if (token == null) {
+            throw new RuntimeException("Please provide token");
+        }
+        this.clientOptionsBuilder.addHeader("Authorization", "Bearer " + this.token);
+        clientOptionsBuilder.environment(this.environment);
+        return new ApiClient(clientOptionsBuilder.build());
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClient.java
new file mode 100644
index 00000000..748eb02e
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClient.java
@@ -0,0 +1,29 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest;
+
+import com.skyflow.generated.auth.rest.core.ClientOptions;
+import com.skyflow.generated.auth.rest.core.Suppliers;
+import com.skyflow.generated.auth.rest.resources.authentication.AsyncAuthenticationClient;
+
+import java.util.function.Supplier;
+
+public class AsyncApiClient {
+    protected final ClientOptions clientOptions;
+
+    protected final Supplier authenticationClient;
+
+    public AsyncApiClient(ClientOptions clientOptions) {
+        this.clientOptions = clientOptions;
+        this.authenticationClient = Suppliers.memoize(() -> new AsyncAuthenticationClient(clientOptions));
+    }
+
+    public AsyncAuthenticationClient authentication() {
+        return this.authenticationClient.get();
+    }
+
+    public static AsyncApiClientBuilder builder() {
+        return new AsyncApiClientBuilder();
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClientBuilder.java b/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClientBuilder.java
new file mode 100644
index 00000000..2e30d45a
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/AsyncApiClientBuilder.java
@@ -0,0 +1,67 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest;
+
+import com.skyflow.generated.auth.rest.core.ClientOptions;
+import com.skyflow.generated.auth.rest.core.Environment;
+import okhttp3.OkHttpClient;
+
+public final class AsyncApiClientBuilder {
+    private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder();
+
+    private String token = null;
+
+    private Environment environment = Environment.PRODUCTION;
+
+    /**
+     * Sets token
+     */
+    public AsyncApiClientBuilder token(String token) {
+        this.token = token;
+        return this;
+    }
+
+    public AsyncApiClientBuilder environment(Environment environment) {
+        this.environment = environment;
+        return this;
+    }
+
+    public AsyncApiClientBuilder url(String url) {
+        this.environment = Environment.custom(url);
+        return this;
+    }
+
+    /**
+     * Sets the timeout (in seconds) for the client. Defaults to 60 seconds.
+     */
+    public AsyncApiClientBuilder timeout(int timeout) {
+        this.clientOptionsBuilder.timeout(timeout);
+        return this;
+    }
+
+    /**
+     * Sets the maximum number of retries for the client. Defaults to 2 retries.
+     */
+    public AsyncApiClientBuilder maxRetries(int maxRetries) {
+        this.clientOptionsBuilder.maxRetries(maxRetries);
+        return this;
+    }
+
+    /**
+     * Sets the underlying OkHttp client
+     */
+    public AsyncApiClientBuilder httpClient(OkHttpClient httpClient) {
+        this.clientOptionsBuilder.httpClient(httpClient);
+        return this;
+    }
+
+    public AsyncApiClient build() {
+        if (token == null) {
+            throw new RuntimeException("Please provide token");
+        }
+        this.clientOptionsBuilder.addHeader("Authorization", "Bearer " + this.token);
+        clientOptionsBuilder.environment(this.environment);
+        return new AsyncApiClient(clientOptionsBuilder.build());
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientApiException.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientApiException.java
new file mode 100644
index 00000000..53d67f0b
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientApiException.java
@@ -0,0 +1,74 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.Response;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This exception type will be thrown for any non-2XX API responses.
+ */
+public class ApiClientApiException extends ApiClientException {
+    /**
+     * The error code of the response that triggered the exception.
+     */
+    private final int statusCode;
+
+    /**
+     * The body of the response that triggered the exception.
+     */
+    private final Object body;
+
+    private final Map> headers;
+
+    public ApiClientApiException(String message, int statusCode, Object body) {
+        super(message);
+        this.statusCode = statusCode;
+        this.body = body;
+        this.headers = new HashMap<>();
+    }
+
+    public ApiClientApiException(String message, int statusCode, Object body, Response rawResponse) {
+        super(message);
+        this.statusCode = statusCode;
+        this.body = body;
+        this.headers = new HashMap<>();
+        rawResponse.headers().forEach(header -> {
+            String key = header.component1();
+            String value = header.component2();
+            this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value);
+        });
+    }
+
+    /**
+     * @return the statusCode
+     */
+    public int statusCode() {
+        return this.statusCode;
+    }
+
+    /**
+     * @return the body
+     */
+    public Object body() {
+        return this.body;
+    }
+
+    /**
+     * @return the headers
+     */
+    public Map> headers() {
+        return this.headers;
+    }
+
+    @Override
+    public String toString() {
+        return "ApiClientApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + ", body: " + body
+                + "}";
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientException.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientException.java
new file mode 100644
index 00000000..f08afa2e
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientException.java
@@ -0,0 +1,17 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+/**
+ * This class serves as the base exception for all errors in the SDK.
+ */
+public class ApiClientException extends RuntimeException {
+    public ApiClientException(String message) {
+        super(message);
+    }
+
+    public ApiClientException(String message, Exception e) {
+        super(message, e);
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientHttpResponse.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientHttpResponse.java
new file mode 100644
index 00000000..8a28d22f
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ApiClientHttpResponse.java
@@ -0,0 +1,38 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.Response;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class ApiClientHttpResponse {
+
+    private final T body;
+
+    private final Map> headers;
+
+    public ApiClientHttpResponse(T body, Response rawResponse) {
+        this.body = body;
+
+        Map> headers = new HashMap<>();
+        rawResponse.headers().forEach(header -> {
+            String key = header.component1();
+            String value = header.component2();
+            headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value);
+        });
+        this.headers = headers;
+    }
+
+    public T body() {
+        return this.body;
+    }
+
+    public Map> headers() {
+        return headers;
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ClientOptions.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ClientOptions.java
new file mode 100644
index 00000000..4eee6b92
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ClientOptions.java
@@ -0,0 +1,171 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.OkHttpClient;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+public final class ClientOptions {
+    private final Environment environment;
+
+    private final Map headers;
+
+    private final Map> headerSuppliers;
+
+    private final OkHttpClient httpClient;
+
+    private final int timeout;
+
+    private ClientOptions(
+            Environment environment,
+            Map headers,
+            Map> headerSuppliers,
+            OkHttpClient httpClient,
+            int timeout) {
+        this.environment = environment;
+        this.headers = new HashMap<>();
+        this.headers.putAll(headers);
+        this.headers.putAll(new HashMap() {
+            {
+                put("X-Fern-Language", "JAVA");
+                put("X-Fern-SDK-Name", "com.skyflow.generated.rest.fern:api-sdk");
+                put("X-Fern-SDK-Version", "0.0.279");
+            }
+        });
+        this.headerSuppliers = headerSuppliers;
+        this.httpClient = httpClient;
+        this.timeout = timeout;
+    }
+
+    public Environment environment() {
+        return this.environment;
+    }
+
+    public Map headers(RequestOptions requestOptions) {
+        Map values = new HashMap<>(this.headers);
+        headerSuppliers.forEach((key, supplier) -> {
+            values.put(key, supplier.get());
+        });
+        if (requestOptions != null) {
+            values.putAll(requestOptions.getHeaders());
+        }
+        return values;
+    }
+
+    public int timeout(RequestOptions requestOptions) {
+        if (requestOptions == null) {
+            return this.timeout;
+        }
+        return requestOptions.getTimeout().orElse(this.timeout);
+    }
+
+    public OkHttpClient httpClient() {
+        return this.httpClient;
+    }
+
+    public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) {
+        if (requestOptions == null) {
+            return this.httpClient;
+        }
+        return this.httpClient
+                .newBuilder()
+                .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit())
+                .connectTimeout(0, TimeUnit.SECONDS)
+                .writeTimeout(0, TimeUnit.SECONDS)
+                .readTimeout(0, TimeUnit.SECONDS)
+                .build();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder {
+        private Environment environment;
+
+        private final Map headers = new HashMap<>();
+
+        private final Map> headerSuppliers = new HashMap<>();
+
+        private int maxRetries = 2;
+
+        private Optional timeout = Optional.empty();
+
+        private OkHttpClient httpClient = null;
+
+        public Builder environment(Environment environment) {
+            this.environment = environment;
+            return this;
+        }
+
+        public Builder addHeader(String key, String value) {
+            this.headers.put(key, value);
+            return this;
+        }
+
+        public Builder addHeader(String key, Supplier value) {
+            this.headerSuppliers.put(key, value);
+            return this;
+        }
+
+        /**
+         * Override the timeout in seconds. Defaults to 60 seconds.
+         */
+        public Builder timeout(int timeout) {
+            this.timeout = Optional.of(timeout);
+            return this;
+        }
+
+        /**
+         * Override the timeout in seconds. Defaults to 60 seconds.
+         */
+        public Builder timeout(Optional timeout) {
+            this.timeout = timeout;
+            return this;
+        }
+
+        /**
+         * Override the maximum number of retries. Defaults to 2 retries.
+         */
+        public Builder maxRetries(int maxRetries) {
+            this.maxRetries = maxRetries;
+            return this;
+        }
+
+        public Builder httpClient(OkHttpClient httpClient) {
+            this.httpClient = httpClient;
+            return this;
+        }
+
+        public ClientOptions build() {
+            OkHttpClient.Builder httpClientBuilder =
+                    this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder();
+
+            if (this.httpClient != null) {
+                timeout.ifPresent(timeout -> httpClientBuilder
+                        .callTimeout(timeout, TimeUnit.SECONDS)
+                        .connectTimeout(0, TimeUnit.SECONDS)
+                        .writeTimeout(0, TimeUnit.SECONDS)
+                        .readTimeout(0, TimeUnit.SECONDS));
+            } else {
+                httpClientBuilder
+                        .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS)
+                        .connectTimeout(0, TimeUnit.SECONDS)
+                        .writeTimeout(0, TimeUnit.SECONDS)
+                        .readTimeout(0, TimeUnit.SECONDS)
+                        .addInterceptor(new RetryInterceptor(this.maxRetries));
+            }
+
+            this.httpClient = httpClientBuilder.build();
+            this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000);
+
+            return new ClientOptions(environment, headers, headerSuppliers, httpClient, this.timeout.get());
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/DateTimeDeserializer.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/DateTimeDeserializer.java
new file mode 100644
index 00000000..dfa25a82
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/DateTimeDeserializer.java
@@ -0,0 +1,56 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalQueries;
+
+/**
+ * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects.
+ */
+class DateTimeDeserializer extends JsonDeserializer {
+    private static final SimpleModule MODULE;
+
+    static {
+        MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer());
+    }
+
+    /**
+     * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper.
+     *
+     * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper.
+     */
+    public static SimpleModule getModule() {
+        return MODULE;
+    }
+
+    @Override
+    public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+        JsonToken token = parser.currentToken();
+        if (token == JsonToken.VALUE_NUMBER_INT) {
+            return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC);
+        } else {
+            TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest(
+                    parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from);
+
+            if (temporal.query(TemporalQueries.offset()) == null) {
+                return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC);
+            } else {
+                return OffsetDateTime.from(temporal);
+            }
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/Environment.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/Environment.java
new file mode 100644
index 00000000..098667c0
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/Environment.java
@@ -0,0 +1,24 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+public final class Environment {
+    public static final Environment PRODUCTION = new Environment("https://manage.skyflowapis.com");
+
+    public static final Environment SANDBOX = new Environment("https://manage.skyflowapis-preview.com");
+
+    private final String url;
+
+    private Environment(String url) {
+        this.url = url;
+    }
+
+    public String getUrl() {
+        return this.url;
+    }
+
+    public static Environment custom(String url) {
+        return new Environment(url);
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/FileStream.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/FileStream.java
new file mode 100644
index 00000000..0e878ed3
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/FileStream.java
@@ -0,0 +1,61 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.InputStream;
+import java.util.Objects;
+
+/**
+ * Represents a file stream with associated metadata for file uploads.
+ */
+public class FileStream {
+    private final InputStream inputStream;
+    private final String fileName;
+    private final MediaType contentType;
+
+    /**
+     * Constructs a FileStream with the given input stream and optional metadata.
+     *
+     * @param inputStream The input stream of the file content. Must not be null.
+     * @param fileName The name of the file, or null if unknown.
+     * @param contentType The MIME type of the file content, or null if unknown.
+     * @throws NullPointerException if inputStream is null
+     */
+    public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) {
+        this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null");
+        this.fileName = fileName;
+        this.contentType = contentType;
+    }
+
+    public FileStream(InputStream inputStream) {
+        this(inputStream, null, null);
+    }
+
+    public InputStream getInputStream() {
+        return inputStream;
+    }
+
+    @Nullable
+    public String getFileName() {
+        return fileName;
+    }
+
+    @Nullable
+    public MediaType getContentType() {
+        return contentType;
+    }
+
+    /**
+     * Creates a RequestBody suitable for use with OkHttp client.
+     *
+     * @return A RequestBody instance representing this file stream.
+     */
+    public RequestBody toRequestBody() {
+        return new InputStreamRequestBody(contentType, inputStream);
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/InputStreamRequestBody.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/InputStreamRequestBody.java
new file mode 100644
index 00000000..a94faec9
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/InputStreamRequestBody.java
@@ -0,0 +1,80 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.internal.Util;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+/**
+ * A custom implementation of OkHttp's RequestBody that wraps an InputStream.
+ * This class allows streaming of data from an InputStream directly to an HTTP request body,
+ * which is useful for file uploads or sending large amounts of data without loading it all into memory.
+ */
+public class InputStreamRequestBody extends RequestBody {
+    private final InputStream inputStream;
+    private final MediaType contentType;
+
+    /**
+     * Constructs an InputStreamRequestBody with the specified content type and input stream.
+     *
+     * @param contentType the MediaType of the content, or null if not known
+     * @param inputStream the InputStream containing the data to be sent
+     * @throws NullPointerException if inputStream is null
+     */
+    public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) {
+        this.contentType = contentType;
+        this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null");
+    }
+
+    /**
+     * Returns the content type of this request body.
+     *
+     * @return the MediaType of the content, or null if not specified
+     */
+    @Nullable
+    @Override
+    public MediaType contentType() {
+        return contentType;
+    }
+
+    /**
+     * Returns the content length of this request body, if known.
+     * This method attempts to determine the length using the InputStream's available() method,
+     * which may not always accurately reflect the total length of the stream.
+     *
+     * @return the content length, or -1 if the length is unknown
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public long contentLength() throws IOException {
+        return inputStream.available() == 0 ? -1 : inputStream.available();
+    }
+
+    /**
+     * Writes the content of the InputStream to the given BufferedSink.
+     * This method is responsible for transferring the data from the InputStream to the network request.
+     *
+     * @param sink the BufferedSink to write the content to
+     * @throws IOException if an I/O error occurs during writing
+     */
+    @Override
+    public void writeTo(BufferedSink sink) throws IOException {
+        Source source = null;
+        try {
+            source = Okio.source(inputStream);
+            sink.writeAll(source);
+        } finally {
+            Util.closeQuietly(Objects.requireNonNull(source));
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/MediaTypes.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/MediaTypes.java
new file mode 100644
index 00000000..d4374647
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/MediaTypes.java
@@ -0,0 +1,13 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.MediaType;
+
+public final class MediaTypes {
+
+    public static final MediaType APPLICATION_JSON = MediaType.parse("application/json");
+
+    private MediaTypes() {}
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/Nullable.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/Nullable.java
new file mode 100644
index 00000000..efabe806
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/Nullable.java
@@ -0,0 +1,140 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import java.util.Optional;
+import java.util.function.Function;
+
+public final class Nullable {
+
+    private final Either, Null> value;
+
+    private Nullable() {
+        this.value = Either.left(Optional.empty());
+    }
+
+    private Nullable(T value) {
+        if (value == null) {
+            this.value = Either.right(Null.INSTANCE);
+        } else {
+            this.value = Either.left(Optional.of(value));
+        }
+    }
+
+    public static  Nullable ofNull() {
+        return new Nullable<>(null);
+    }
+
+    public static  Nullable of(T value) {
+        return new Nullable<>(value);
+    }
+
+    public static  Nullable empty() {
+        return new Nullable<>();
+    }
+
+    public static  Nullable ofOptional(Optional value) {
+        if (value.isPresent()) {
+            return of(value.get());
+        } else {
+            return empty();
+        }
+    }
+
+    public boolean isNull() {
+        return this.value.isRight();
+    }
+
+    public boolean isEmpty() {
+        return this.value.isLeft() && !this.value.getLeft().isPresent();
+    }
+
+    public T get() {
+        if (this.isNull()) {
+            return null;
+        }
+
+        return this.value.getLeft().get();
+    }
+
+    public  Nullable map(Function mapper) {
+        if (this.isNull()) {
+            return Nullable.ofNull();
+        }
+
+        return Nullable.ofOptional(this.value.getLeft().map(mapper));
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof Nullable)) {
+            return false;
+        }
+
+        if (((Nullable) other).isNull() && this.isNull()) {
+            return true;
+        }
+
+        return this.value.getLeft().equals(((Nullable) other).value.getLeft());
+    }
+
+    private static final class Either {
+        private L left = null;
+        private R right = null;
+
+        private Either(L left, R right) {
+            if (left != null && right != null) {
+                throw new IllegalArgumentException("Left and right argument cannot both be non-null.");
+            }
+
+            if (left == null && right == null) {
+                throw new IllegalArgumentException("Left and right argument cannot both be null.");
+            }
+
+            if (left != null) {
+                this.left = left;
+            }
+
+            if (right != null) {
+                this.right = right;
+            }
+        }
+
+        public static  Either left(L left) {
+            return new Either<>(left, null);
+        }
+
+        public static  Either right(R right) {
+            return new Either<>(null, right);
+        }
+
+        public boolean isLeft() {
+            return this.left != null;
+        }
+
+        public boolean isRight() {
+            return this.right != null;
+        }
+
+        public L getLeft() {
+            if (!this.isLeft()) {
+                throw new IllegalArgumentException("Cannot get left from right Either.");
+            }
+            return this.left;
+        }
+
+        public R getRight() {
+            if (!this.isRight()) {
+                throw new IllegalArgumentException("Cannot get right from left Either.");
+            }
+            return this.right;
+        }
+    }
+
+    private static final class Null {
+        private static final Null INSTANCE = new Null();
+
+        private Null() {}
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/NullableNonemptyFilter.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/NullableNonemptyFilter.java
new file mode 100644
index 00000000..dd32d66c
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/NullableNonemptyFilter.java
@@ -0,0 +1,19 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import java.util.Optional;
+
+public final class NullableNonemptyFilter {
+    @Override
+    public boolean equals(Object o) {
+        boolean isOptionalEmpty = isOptionalEmpty(o);
+
+        return isOptionalEmpty;
+    }
+
+    private boolean isOptionalEmpty(Object o) {
+        return o instanceof Optional && !((Optional) o).isPresent();
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ObjectMappers.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ObjectMappers.java
new file mode 100644
index 00000000..4997b3ca
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ObjectMappers.java
@@ -0,0 +1,37 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import java.io.IOException;
+
+public final class ObjectMappers {
+    public static final ObjectMapper JSON_MAPPER = JsonMapper.builder()
+            .addModule(new Jdk8Module())
+            .addModule(new JavaTimeModule())
+            .addModule(DateTimeDeserializer.getModule())
+            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+            .build();
+
+    private ObjectMappers() {}
+
+    public static String stringify(Object o) {
+        try {
+            return JSON_MAPPER
+                    .setSerializationInclusion(JsonInclude.Include.ALWAYS)
+                    .writerWithDefaultPrettyPrinter()
+                    .writeValueAsString(o);
+        } catch (IOException e) {
+            return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode());
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/QueryStringMapper.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/QueryStringMapper.java
new file mode 100644
index 00000000..8b1f9f7e
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/QueryStringMapper.java
@@ -0,0 +1,139 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import okhttp3.HttpUrl;
+import okhttp3.MultipartBody;
+
+import java.util.*;
+
+public class QueryStringMapper {
+
+    private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER;
+
+    public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) {
+        JsonNode valueNode = MAPPER.valueToTree(value);
+
+        List> flat;
+        if (valueNode.isObject()) {
+            flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats);
+        } else if (valueNode.isArray()) {
+            flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats);
+        } else {
+            if (valueNode.isTextual()) {
+                httpUrl.addQueryParameter(key, valueNode.textValue());
+            } else {
+                httpUrl.addQueryParameter(key, valueNode.toString());
+            }
+            return;
+        }
+
+        for (Map.Entry field : flat) {
+            if (field.getValue().isTextual()) {
+                httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue());
+            } else {
+                httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString());
+            }
+        }
+    }
+
+    public static void addFormDataPart(
+            MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) {
+        JsonNode valueNode = MAPPER.valueToTree(value);
+
+        List> flat;
+        if (valueNode.isObject()) {
+            flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats);
+        } else if (valueNode.isArray()) {
+            flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats);
+        } else {
+            if (valueNode.isTextual()) {
+                multipartBody.addFormDataPart(key, valueNode.textValue());
+            } else {
+                multipartBody.addFormDataPart(key, valueNode.toString());
+            }
+            return;
+        }
+
+        for (Map.Entry field : flat) {
+            if (field.getValue().isTextual()) {
+                multipartBody.addFormDataPart(
+                        key + field.getKey(), field.getValue().textValue());
+            } else {
+                multipartBody.addFormDataPart(
+                        key + field.getKey(), field.getValue().toString());
+            }
+        }
+    }
+
+    public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) {
+        List> flat = new ArrayList<>();
+
+        Iterator> fields = object.fields();
+        while (fields.hasNext()) {
+            Map.Entry field = fields.next();
+
+            String key = "[" + field.getKey() + "]";
+
+            if (field.getValue().isObject()) {
+                List> flatField =
+                        flattenObject((ObjectNode) field.getValue(), arraysAsRepeats);
+                addAll(flat, flatField, key);
+            } else if (field.getValue().isArray()) {
+                List> flatField =
+                        flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats);
+                addAll(flat, flatField, "");
+            } else {
+                flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue()));
+            }
+        }
+
+        return flat;
+    }
+
+    private static List> flattenArray(
+            ArrayNode array, String key, boolean arraysAsRepeats) {
+        List> flat = new ArrayList<>();
+
+        Iterator elements = array.elements();
+
+        int index = 0;
+        while (elements.hasNext()) {
+            JsonNode element = elements.next();
+
+            String indexKey = key + "[" + index + "]";
+
+            if (arraysAsRepeats) {
+                indexKey = key;
+            }
+
+            if (element.isObject()) {
+                List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats);
+                addAll(flat, flatField, indexKey);
+            } else if (element.isArray()) {
+                List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats);
+                addAll(flat, flatField, indexKey);
+            } else {
+                flat.add(new AbstractMap.SimpleEntry<>(indexKey, element));
+            }
+
+            index++;
+        }
+
+        return flat;
+    }
+
+    private static void addAll(
+            List> target, List> source, String prefix) {
+        for (Map.Entry entry : source) {
+            Map.Entry entryToAdd =
+                    new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue());
+            target.add(entryToAdd);
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/RequestOptions.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/RequestOptions.java
new file mode 100644
index 00000000..b70c02df
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/RequestOptions.java
@@ -0,0 +1,101 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+public final class RequestOptions {
+    private final String token;
+
+    private final Optional timeout;
+
+    private final TimeUnit timeoutTimeUnit;
+
+    private final Map headers;
+
+    private final Map> headerSuppliers;
+
+    private RequestOptions(
+            String token,
+            Optional timeout,
+            TimeUnit timeoutTimeUnit,
+            Map headers,
+            Map> headerSuppliers) {
+        this.token = token;
+        this.timeout = timeout;
+        this.timeoutTimeUnit = timeoutTimeUnit;
+        this.headers = headers;
+        this.headerSuppliers = headerSuppliers;
+    }
+
+    public Optional getTimeout() {
+        return timeout;
+    }
+
+    public TimeUnit getTimeoutTimeUnit() {
+        return timeoutTimeUnit;
+    }
+
+    public Map getHeaders() {
+        Map headers = new HashMap<>();
+        if (this.token != null) {
+            headers.put("Authorization", "Bearer " + this.token);
+        }
+        headers.putAll(this.headers);
+        this.headerSuppliers.forEach((key, supplier) -> {
+            headers.put(key, supplier.get());
+        });
+        return headers;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static final class Builder {
+        private String token = null;
+
+        private Optional timeout = Optional.empty();
+
+        private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS;
+
+        private final Map headers = new HashMap<>();
+
+        private final Map> headerSuppliers = new HashMap<>();
+
+        public Builder token(String token) {
+            this.token = token;
+            return this;
+        }
+
+        public Builder timeout(Integer timeout) {
+            this.timeout = Optional.of(timeout);
+            return this;
+        }
+
+        public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) {
+            this.timeout = Optional.of(timeout);
+            this.timeoutTimeUnit = timeoutTimeUnit;
+            return this;
+        }
+
+        public Builder addHeader(String key, String value) {
+            this.headers.put(key, value);
+            return this;
+        }
+
+        public Builder addHeader(String key, Supplier value) {
+            this.headerSuppliers.put(key, value);
+            return this;
+        }
+
+        public RequestOptions build() {
+            return new RequestOptions(token, timeout, timeoutTimeUnit, headers, headerSuppliers);
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyInputStream.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyInputStream.java
new file mode 100644
index 00000000..1d0a9110
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyInputStream.java
@@ -0,0 +1,46 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.Response;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+
+/**
+ * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the
+ * OkHttp Response object is properly closed when the stream is closed.
+ *
+ * This class extends FilterInputStream and takes an OkHttp Response object as a parameter.
+ * It retrieves the InputStream from the Response and overrides the close method to close
+ * both the InputStream and the Response object, ensuring proper resource management and preventing
+ * premature closure of the underlying HTTP connection.
+ */
+public class ResponseBodyInputStream extends FilterInputStream {
+    private final Response response;
+
+    /**
+     * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp
+     * Response object.
+     *
+     * @param response the OkHttp Response object from which the InputStream is retrieved
+     * @throws IOException if an I/O error occurs while retrieving the InputStream
+     */
+    public ResponseBodyInputStream(Response response) throws IOException {
+        super(response.body().byteStream());
+        this.response = response;
+    }
+
+    /**
+     * Closes the InputStream and the associated OkHttp Response object. This ensures that the
+     * underlying HTTP connection is properly closed after the stream is no longer needed.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        super.close();
+        response.close(); // Ensure the response is closed when the stream is closed
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyReader.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyReader.java
new file mode 100644
index 00000000..f4dfb17b
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/ResponseBodyReader.java
@@ -0,0 +1,45 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.Response;
+
+import java.io.FilterReader;
+import java.io.IOException;
+
+/**
+ * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the
+ * OkHttp Response object is properly closed when the reader is closed.
+ *
+ * This class extends FilterReader and takes an OkHttp Response object as a parameter.
+ * It retrieves the Reader from the Response and overrides the close method to close
+ * both the Reader and the Response object, ensuring proper resource management and preventing
+ * premature closure of the underlying HTTP connection.
+ */
+public class ResponseBodyReader extends FilterReader {
+    private final Response response;
+
+    /**
+     * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object.
+     *
+     * @param response the OkHttp Response object from which the Reader is retrieved
+     * @throws IOException if an I/O error occurs while retrieving the Reader
+     */
+    public ResponseBodyReader(Response response) throws IOException {
+        super(response.body().charStream());
+        this.response = response;
+    }
+
+    /**
+     * Closes the Reader and the associated OkHttp Response object. This ensures that the
+     * underlying HTTP connection is properly closed after the reader is no longer needed.
+     *
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    public void close() throws IOException {
+        super.close();
+        response.close(); // Ensure the response is closed when the reader is closed
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/RetryInterceptor.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/RetryInterceptor.java
new file mode 100644
index 00000000..8344b974
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/RetryInterceptor.java
@@ -0,0 +1,79 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import okhttp3.Interceptor;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Optional;
+import java.util.Random;
+
+public class RetryInterceptor implements Interceptor {
+
+    private static final Duration ONE_SECOND = Duration.ofSeconds(1);
+    private final ExponentialBackoff backoff;
+    private final Random random = new Random();
+
+    public RetryInterceptor(int maxRetries) {
+        this.backoff = new ExponentialBackoff(maxRetries);
+    }
+
+    @Override
+    public Response intercept(Chain chain) throws IOException {
+        Response response = chain.proceed(chain.request());
+
+        if (shouldRetry(response.code())) {
+            return retryChain(response, chain);
+        }
+
+        return response;
+    }
+
+    private Response retryChain(Response response, Chain chain) throws IOException {
+        Optional nextBackoff = this.backoff.nextBackoff();
+        while (nextBackoff.isPresent()) {
+            try {
+                Thread.sleep(nextBackoff.get().toMillis());
+            } catch (InterruptedException e) {
+                throw new IOException("Interrupted while trying request", e);
+            }
+            response.close();
+            response = chain.proceed(chain.request());
+            if (shouldRetry(response.code())) {
+                nextBackoff = this.backoff.nextBackoff();
+            } else {
+                return response;
+            }
+        }
+
+        return response;
+    }
+
+    private static boolean shouldRetry(int statusCode) {
+        return statusCode == 408 || statusCode == 429 || statusCode >= 500;
+    }
+
+    private final class ExponentialBackoff {
+
+        private final int maxNumRetries;
+
+        private int retryNumber = 0;
+
+        ExponentialBackoff(int maxNumRetries) {
+            this.maxNumRetries = maxNumRetries;
+        }
+
+        public Optional nextBackoff() {
+            retryNumber += 1;
+            if (retryNumber > maxNumRetries) {
+                return Optional.empty();
+            }
+
+            int upperBound = (int) Math.pow(2, retryNumber);
+            return Optional.of(ONE_SECOND.multipliedBy(random.nextInt(upperBound)));
+        }
+    }
+}
diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/Stream.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/Stream.java
new file mode 100644
index 00000000..4a984faa
--- /dev/null
+++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/Stream.java
@@ -0,0 +1,97 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.skyflow.generated.auth.rest.core;
+
+import java.io.Reader;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+
+/**
+ * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing
+ * objects of a given type from data streamed via a {@link Reader} using a specified delimiter.
+ * 

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable { + /** + * The {@link Class} of the objects in the stream. + */ + private final Class valueType; + /** + * The {@link Scanner} used for reading from the input stream and blocking when needed during iteration. + */ + private final Scanner scanner; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.valueType = valueType; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + return new Iterator() { + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = ObjectMappers.JSON_MAPPER.readValue( + scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Removing elements from {@code Stream} is not supported. + * + * @throws UnsupportedOperationException Always, as removal is not supported. + */ + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/core/Suppliers.java b/common/src/main/java/com/skyflow/generated/auth/rest/core/Suppliers.java new file mode 100644 index 00000000..d3ab5e53 --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.auth.rest.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/errors/BadRequestError.java b/common/src/main/java/com/skyflow/generated/auth/rest/errors/BadRequestError.java new file mode 100644 index 00000000..4517200d --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/errors/BadRequestError.java @@ -0,0 +1,34 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.auth.rest.errors; + +import com.skyflow.generated.auth.rest.core.ApiClientApiException; +import okhttp3.Response; + +import java.util.Map; + +public final class BadRequestError extends ApiClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Map body; + + public BadRequestError(Map body) { + super("BadRequestError", 400, body); + this.body = body; + } + + public BadRequestError(Map body, Response rawResponse) { + super("BadRequestError", 400, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @Override + public Map body() { + return this.body; + } +} diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/errors/HttpStatus.java b/common/src/main/java/com/skyflow/generated/auth/rest/errors/HttpStatus.java new file mode 100644 index 00000000..2e1c45d2 --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/errors/HttpStatus.java @@ -0,0 +1,15 @@ +package com.skyflow.generated.auth.rest.errors; + +public enum HttpStatus { + BAD_REQUEST("Bad Request"); + + private final String httpStatus; + + HttpStatus(String httpStatus) { + this.httpStatus = httpStatus; + } + + public String getHttpStatus() { + return httpStatus; + } +} diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/errors/NotFoundError.java b/common/src/main/java/com/skyflow/generated/auth/rest/errors/NotFoundError.java new file mode 100644 index 00000000..60a1771c --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/errors/NotFoundError.java @@ -0,0 +1,34 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.auth.rest.errors; + +import com.skyflow.generated.auth.rest.core.ApiClientApiException; +import okhttp3.Response; + +import java.util.Map; + +public final class NotFoundError extends ApiClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Map body; + + public NotFoundError(Map body) { + super("NotFoundError", 404, body); + this.body = body; + } + + public NotFoundError(Map body, Response rawResponse) { + super("NotFoundError", 404, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @Override + public Map body() { + return this.body; + } +} diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/errors/UnauthorizedError.java b/common/src/main/java/com/skyflow/generated/auth/rest/errors/UnauthorizedError.java new file mode 100644 index 00000000..42f1c624 --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/errors/UnauthorizedError.java @@ -0,0 +1,34 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.auth.rest.errors; + +import com.skyflow.generated.auth.rest.core.ApiClientApiException; +import okhttp3.Response; + +import java.util.Map; + +public final class UnauthorizedError extends ApiClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Map body; + + public UnauthorizedError(Map body) { + super("UnauthorizedError", 401, body); + this.body = body; + } + + public UnauthorizedError(Map body, Response rawResponse) { + super("UnauthorizedError", 401, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @Override + public Map body() { + return this.body; + } +} diff --git a/src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncAuthenticationClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncAuthenticationClient.java similarity index 84% rename from src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncAuthenticationClient.java rename to common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncAuthenticationClient.java index 43ffab73..c742b239 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncAuthenticationClient.java +++ b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncAuthenticationClient.java @@ -1,12 +1,13 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.skyflow.generated.rest.resources.authentication; +package com.skyflow.generated.auth.rest.resources.authentication; + +import com.skyflow.generated.auth.rest.core.ClientOptions; +import com.skyflow.generated.auth.rest.core.RequestOptions; +import com.skyflow.generated.auth.rest.resources.authentication.requests.V1GetAuthTokenRequest; +import com.skyflow.generated.auth.rest.types.V1GetAuthTokenResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.authentication.requests.V1GetAuthTokenRequest; -import com.skyflow.generated.rest.types.V1GetAuthTokenResponse; import java.util.concurrent.CompletableFuture; public class AsyncAuthenticationClient { diff --git a/src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncRawAuthenticationClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncRawAuthenticationClient.java similarity index 82% rename from src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncRawAuthenticationClient.java rename to common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncRawAuthenticationClient.java index eca4ab90..56a47dc0 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/authentication/AsyncRawAuthenticationClient.java +++ b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AsyncRawAuthenticationClient.java @@ -1,33 +1,22 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.skyflow.generated.rest.resources.authentication; +package com.skyflow.generated.auth.rest.resources.authentication; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.errors.BadRequestError; -import com.skyflow.generated.rest.errors.NotFoundError; -import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.authentication.requests.V1GetAuthTokenRequest; -import com.skyflow.generated.rest.types.V1GetAuthTokenResponse; +import com.fasterxml.jackson.core.type.TypeReference; +import com.skyflow.generated.auth.rest.core.*; +import com.skyflow.generated.auth.rest.errors.BadRequestError; +import com.skyflow.generated.auth.rest.errors.NotFoundError; +import com.skyflow.generated.auth.rest.errors.UnauthorizedError; +import com.skyflow.generated.auth.rest.resources.authentication.requests.V1GetAuthTokenRequest; +import com.skyflow.generated.auth.rest.types.V1GetAuthTokenResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; +import java.util.Map; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawAuthenticationClient { protected final ClientOptions clientOptions; @@ -88,17 +77,20 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO switch (response.code()) { case 400: future.completeExceptionally(new BadRequestError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), response)); return; case 401: future.completeExceptionally(new UnauthorizedError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), response)); return; case 404: future.completeExceptionally(new NotFoundError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), response)); return; } diff --git a/src/main/java/com/skyflow/generated/rest/resources/authentication/AuthenticationClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AuthenticationClient.java similarity index 82% rename from src/main/java/com/skyflow/generated/rest/resources/authentication/AuthenticationClient.java rename to common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AuthenticationClient.java index 662bfb3d..a0ba860a 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/authentication/AuthenticationClient.java +++ b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/AuthenticationClient.java @@ -1,12 +1,12 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.skyflow.generated.rest.resources.authentication; +package com.skyflow.generated.auth.rest.resources.authentication; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.authentication.requests.V1GetAuthTokenRequest; -import com.skyflow.generated.rest.types.V1GetAuthTokenResponse; +import com.skyflow.generated.auth.rest.core.ClientOptions; +import com.skyflow.generated.auth.rest.core.RequestOptions; +import com.skyflow.generated.auth.rest.resources.authentication.requests.V1GetAuthTokenRequest; +import com.skyflow.generated.auth.rest.types.V1GetAuthTokenResponse; public class AuthenticationClient { protected final ClientOptions clientOptions; diff --git a/src/main/java/com/skyflow/generated/rest/resources/authentication/RawAuthenticationClient.java b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/RawAuthenticationClient.java similarity index 78% rename from src/main/java/com/skyflow/generated/rest/resources/authentication/RawAuthenticationClient.java rename to common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/RawAuthenticationClient.java index 3d242eff..6c52c048 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/authentication/RawAuthenticationClient.java +++ b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/RawAuthenticationClient.java @@ -1,29 +1,20 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.skyflow.generated.rest.resources.authentication; +package com.skyflow.generated.auth.rest.resources.authentication; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.errors.BadRequestError; -import com.skyflow.generated.rest.errors.NotFoundError; -import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.authentication.requests.V1GetAuthTokenRequest; -import com.skyflow.generated.rest.types.V1GetAuthTokenResponse; +import com.fasterxml.jackson.core.type.TypeReference; +import com.skyflow.generated.auth.rest.core.*; +import com.skyflow.generated.auth.rest.errors.BadRequestError; +import com.skyflow.generated.auth.rest.errors.NotFoundError; +import com.skyflow.generated.auth.rest.errors.UnauthorizedError; +import com.skyflow.generated.auth.rest.resources.authentication.requests.V1GetAuthTokenRequest; +import com.skyflow.generated.auth.rest.types.V1GetAuthTokenResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; +import java.util.Map; public class RawAuthenticationClient { protected final ClientOptions clientOptions; @@ -79,13 +70,19 @@ public ApiClientHttpResponse authenticationServiceGetAut switch (response.code()) { case 400: throw new BadRequestError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), + response); case 401: throw new UnauthorizedError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), + response); case 404: throw new NotFoundError( - ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), + response); } } catch (JsonProcessingException ignored) { // unable to map error response, throwing generic error diff --git a/src/main/java/com/skyflow/generated/rest/resources/authentication/requests/V1GetAuthTokenRequest.java b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/requests/V1GetAuthTokenRequest.java similarity index 96% rename from src/main/java/com/skyflow/generated/rest/resources/authentication/requests/V1GetAuthTokenRequest.java rename to common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/requests/V1GetAuthTokenRequest.java index 182952bc..382c5431 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/authentication/requests/V1GetAuthTokenRequest.java +++ b/common/src/main/java/com/skyflow/generated/auth/rest/resources/authentication/requests/V1GetAuthTokenRequest.java @@ -1,22 +1,17 @@ /** * This file was auto-generated by Fern from our API Definition. */ -package com.skyflow.generated.rest.resources.authentication.requests; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +package com.skyflow.generated.auth.rest.resources.authentication.requests; + +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.auth.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1GetAuthTokenRequest.Builder.class) diff --git a/common/src/main/java/com/skyflow/generated/auth/rest/types/V1GetAuthTokenResponse.java b/common/src/main/java/com/skyflow/generated/auth/rest/types/V1GetAuthTokenResponse.java new file mode 100644 index 00000000..3db2f22b --- /dev/null +++ b/common/src/main/java/com/skyflow/generated/auth/rest/types/V1GetAuthTokenResponse.java @@ -0,0 +1,125 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.auth.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.auth.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1GetAuthTokenResponse.Builder.class) +public final class V1GetAuthTokenResponse { + private final Optional accessToken; + + private final Optional tokenType; + + private final Map additionalProperties; + + private V1GetAuthTokenResponse( + Optional accessToken, Optional tokenType, Map additionalProperties) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.additionalProperties = additionalProperties; + } + + /** + * @return AccessToken. + */ + @JsonProperty("accessToken") + public Optional getAccessToken() { + return accessToken; + } + + /** + * @return TokenType : Bearer. + */ + @JsonProperty("tokenType") + public Optional getTokenType() { + return tokenType; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1GetAuthTokenResponse && equalTo((V1GetAuthTokenResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1GetAuthTokenResponse other) { + return accessToken.equals(other.accessToken) && tokenType.equals(other.tokenType); + } + + @Override + public int hashCode() { + return Objects.hash(this.accessToken, this.tokenType); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional accessToken = Optional.empty(); + + private Optional tokenType = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1GetAuthTokenResponse other) { + accessToken(other.getAccessToken()); + tokenType(other.getTokenType()); + return this; + } + + /** + *

AccessToken.

+ */ + @JsonSetter(value = "accessToken", nulls = Nulls.SKIP) + public Builder accessToken(Optional accessToken) { + this.accessToken = accessToken; + return this; + } + + public Builder accessToken(String accessToken) { + this.accessToken = Optional.ofNullable(accessToken); + return this; + } + + /** + *

TokenType : Bearer.

+ */ + @JsonSetter(value = "tokenType", nulls = Nulls.SKIP) + public Builder tokenType(Optional tokenType) { + this.tokenType = tokenType; + return this; + } + + public Builder tokenType(String tokenType) { + this.tokenType = Optional.ofNullable(tokenType); + return this; + } + + public V1GetAuthTokenResponse build() { + return new V1GetAuthTokenResponse(accessToken, tokenType, additionalProperties); + } + } +} diff --git a/src/main/java/com/skyflow/logs/ErrorLogs.java b/common/src/main/java/com/skyflow/logs/ErrorLogs.java similarity index 68% rename from src/main/java/com/skyflow/logs/ErrorLogs.java rename to common/src/main/java/com/skyflow/logs/ErrorLogs.java index 47866efc..1a405bb5 100644 --- a/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/common/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -8,6 +8,7 @@ public enum ErrorLogs { EMPTY_VAULT_ID("Invalid vault config. Vault ID can not be empty."), CLUSTER_ID_IS_REQUIRED("Invalid vault config. Cluster ID is required."), EMPTY_CLUSTER_ID("Invalid vault config. Cluster ID can not be empty."), + EITHER_VAULT_URL_OR_CLUSTER_ID_REQUIRED("Invalid vault config. At least one of \"clusterId\" or \"vaultURL\" must be provided."), CONNECTION_CONFIG_EXISTS("Connection config with connection ID %s1 already exists."), CONNECTION_CONFIG_DOES_NOT_EXIST("Connection config with connection ID %s1 doesn't exist."), CONNECTION_ID_IS_REQUIRED("Invalid connection config. Connection ID is required."), @@ -27,6 +28,8 @@ public enum ErrorLogs { EMPTY_OR_NULL_CONTEXT("Invalid credentials. Context can not be empty."), INVALID_CONTEXT_TYPE("Invalid credentials. Context must be a String or Map."), INVALID_CONTEXT_MAP_KEY("Invalid credentials. Context map key '%s1' contains invalid characters."), + EMPTY_VAULT_URL("Invalid vault config. Vault URL can not be empty."), + INVALID_VAULT_URL_FORMAT("Invalid vault config. Vault URL format is incorrect"), // Bearer token generation INVALID_BEARER_TOKEN("Bearer token is invalid or expired."), @@ -50,12 +53,23 @@ public enum ErrorLogs { TABLE_IS_REQUIRED("Invalid %s1 request. Table is required."), EMPTY_TABLE_NAME("Invalid %s1 request. Table name can not be empty."), VALUES_IS_REQUIRED("Invalid %s1 request. Values are required."), + EMPTY_VALUES("Invalid %s1 request. Values can not be empty."), + INSERT_REQUEST_NULL("Invalid %s1 request. Insert request can not be null."), + RECORDS_IS_REQUIRED("Invalid %s1 request. Records are required."), + EMPTY_RECORDS("Invalid %s1 request. Records can not be empty."), + INVALID_RECORD("Invalid %s1 request. Invalid record. Specify a valid record."), + RECORD_SIZE_EXCEED("Maximum number of records exceeded. The limit is 10000."), + TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 10000."), + EMPTY_OR_NULL_VALUE_IN_VALUES("Invalid %s1 request. Value can not be null or empty in values for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_VALUES("Invalid %s1 request. Key can not be null or empty in values"), EMPTY_UPSERT("Invalid %s1 request. Upsert can not be empty."), + INVALID_UPSERT_UPDATE_TYPE("Invalid %s1 request. Upsert updateType must be either UPDATE or REPLACE."), + EMPTY_UPSERT_VALUES("Invalid %s1 request. Upsert values can not be empty."), HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT("Invalid %s1 request. Homogenous is not supported when upsert is passed."), TOKENS_NOT_ALLOWED_WITH_TOKEN_MODE_DISABLE("Invalid %s1 request. Tokens are not allowed when tokenMode is DISABLE."), TOKENS_REQUIRED_WITH_TOKEN_MODE("Invalid %s1 request. Tokens are required when tokenMode is %s2."), EMPTY_TOKENS("Invalid %s1 request. Tokens can not be empty."), + EMPTY_OR_NULL_VALUE_IN_TOKENS("Invalid %s1 request. Value can not be null or empty in tokens for key \"%s2\"."), EMPTY_OR_NULL_KEY_IN_TOKENS("Invalid %s1 request. Key can not be null or empty in tokens."), INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT("Invalid %s1 request. For tokenMode as ENABLE_STRICT, tokens should be passed for all fields."), MISMATCH_OF_FIELDS_AND_TOKENS("Invalid %s1 request. Keys for values and tokens are not matching."), @@ -65,6 +79,15 @@ public enum ErrorLogs { EMPTY_OR_NULL_TOKEN_IN_DETOKENIZE_DATA("Invalid %s1 request. Token can not be null or empty in detokenize data at index %s2."), REDACTION_IS_REQUIRED("Invalid %s1 request. Redaction is required."), DETOKENIZE_REQUEST_REJECTED("Detokenize request resulted in failure."), + DETOKENIZE_REQUEST_NULL("Invalid %s1 request. Detokenize request can not be null."), + + NULL_TOKEN_REDACTION_GROUP_OBJECT("Invalid %s1 request. Token Redaction group object can not be null or empty."), + + NULL_REDACTION_IN_TOKEN_GROUP("Invalid %s1 request. Redaction can not be null in token redaction group"), + + NULL_TOKEN_GROUP_NAME_IN_TOKEN_GROUP("Invalid %s1 request. Token group name can not be null in token redaction group"), + + EMPTY_OR_NULL_REDACTION_IN_TOKEN_GROUP("Invalid %s1 request. Redaction can not be null or empty in token redaction group"), IDS_IS_REQUIRED("Invalid %s1 request. Ids are required."), EMPTY_IDS("Invalid %s1 request. Ids can not be empty."), EMPTY_OR_NULL_ID_IN_IDS("Invalid %s1 request. Id can not be null or empty in ids at index %s2."), @@ -82,6 +105,15 @@ public enum ErrorLogs { EMPTY_COLUMN_VALUES("Invalid %s1 request. Column values can not be empty."), EMPTY_OR_NULL_COLUMN_VALUE_IN_COLUMN_VALUES("Invalid %s1 request. Column value can not by null or empty in column values at index %s2."), GET_REQUEST_REJECTED("Get request resulted in failure."), + NEITHER_IDS_NOR_UNIQUE_VALUES_PASSED("Invalid %s1 request. Neither ids nor unique values are passed."), + BOTH_IDS_AND_UNIQUE_VALUES_PASSED("Invalid %s1 request. Both ids and unique values are passed."), + EMPTY_UNIQUE_VALUES("Invalid %s1 request. Unique values can not be empty."), + EMPTY_OR_NULL_UNIQUE_VALUE_IN_UNIQUE_VALUES("Invalid %s1 request. Unique value can not be null or empty in unique values at index %s2."), + NULL_COLUMN_REDACTION_OBJECT("Invalid %s1 request. Column redaction object can not be null."), + NULL_COLUMN_NAME_IN_COLUMN_REDACTION("Invalid %s1 request. Column name can not be null or empty in column redaction."), + EMPTY_OR_NULL_REDACTION_IN_COLUMN_REDACTION("Invalid %s1 request. Redaction can not be null or empty in column redaction."), + BOTH_SINGLE_TABLE_FIELDS_AND_RECORDS_PASSED("Invalid %s1 request. Both single-table lookup fields and records are passed."), + NULL_GET_RECORD_REQUEST_OBJECT("Invalid %s1 request. Record in records list can not be null."), DATA_IS_REQUIRED("Invalid %s1 request. Data is required."), EMPTY_DATA("Invalid %s1 request. Data can not be empty."), SKYFLOW_ID_IS_REQUIRED("Invalid %s1 request. Skyflow Id is required."), @@ -94,7 +126,21 @@ public enum ErrorLogs { COLUMN_VALUES_IS_REQUIRED_TOKENIZE("Invalid %s1 request. ColumnValues are required."), EMPTY_OR_NULL_COLUMN_GROUP_IN_COLUMN_VALUES("Invalid %s1 request. Column group can not be null or empty in column values at index %s2."), TOKENIZE_REQUEST_REJECTED("Tokenize request resulted in failure."), + TOKENIZE_REQUEST_NULL("Invalid %s1 request. Tokenize request can not be null."), + EMPTY_TOKENIZE_DATA("Invalid %s1 request. Tokenize data can not be empty."), + TOKENIZE_RECORD_NULL("Invalid %s1 request. TokenizeRecord in list can not be null."), + EMPTY_VALUE_IN_TOKENIZE_RECORD("Invalid %s1 request. Value in TokenizeRecord can not be null or empty."), + EMPTY_TOKEN_GROUP_NAMES_IN_TOKENIZE_RECORD("Invalid %s1 request. TokenGroupNames in TokenizeRecord can not be null or empty."), + EMPTY_TOKEN_GROUP_NAME_IN_TOKENIZE_RECORD("Invalid %s1 request. Token group name in TokenizeRecord can not be null or empty at index %s2."), + TOKENIZE_DATA_SIZE_EXCEED("Maximum number of tokenize records exceeded. The limit is 10000."), + 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."), + EMPTY_OR_NULL_TOKEN_IN_DELETE_TOKENS_DATA("Invalid %s1 request. Token can not be null or empty in delete tokens data at index %s2."), + DELETE_TOKENS_SIZE_EXCEED("Maximum number of tokens exceeded. The limit is 10000."), + DELETE_TOKENS_REQUEST_REJECTED("DeleteTokens request resulted in failure."), // invoke connection interface INVOKE_CONNECTION_INVALID_CONNECTION_URL("Invalid %s1 request. Connection URL is not a valid URL."), @@ -117,7 +163,6 @@ public enum ErrorLogs { MISSING_FILE_SOURCE_IN_UPLOAD_FILE("Invalid %s1 request. Provide exactly one of filePath, base64, or fileObject."), UPLOAD_FILE_REQUEST_REJECTED("Upload file request resulted in failure."), - // detect interface INVALID_TEXT_IN_DEIDENTIFY("Invalid %s1 request. The text field is required and must be a non-empty string. Specify a valid text."), DEIDENTIFY_TEXT_REQUEST_REJECTED("DeIdentify text request resulted in failure."), @@ -134,8 +179,14 @@ public enum ErrorLogs { OUTPUT_DIRECTORY_NOT_FOUND("Invalid %s1 request. The output directory does not exist. Please specify a valid output directory."), INVALID_PERMISSIONS_FOR_OUTPUT_DIRECTORY("Invalid %s1 request. The output directory is not writable. Please check the permissions or specify a valid output directory."), EMPTY_FILE_AND_FILE_PATH_IN_DEIDENTIFY_FILE("Invalid %s1 request. The file and file path fields are both empty. Specify a valid file object or file path."), - ; + UNEXPECTED_ERROR_DURING_BATCH_PROCESSING("Unexpected error occurred during batch processing. Error: %s1"), + + PROCESSING_ERROR_RESPONSE("Processing error response."), + TABLE_SPECIFIED_AT_BOTH_PLACE("Invalid %s1 request. Table name cannot be specified at both the request and record levels. Please specify the table name at only one place."), + TABLE_NOT_SPECIFIED_AT_BOTH_PLACE("Invalid %s1 request. Table name is missing. Table name should be specified at one place either at the request level or record level. Please specify the table name at one place."), + UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL("Invalid %s1 request. Table name should be present at each record level when upsert is present at record level."), + UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL("Invalid %s1 request. Upsert should be present at each record level when table name is present at record level."); private final String log; ErrorLogs(String log) { diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/common/src/main/java/com/skyflow/logs/InfoLogs.java similarity index 92% rename from src/main/java/com/skyflow/logs/InfoLogs.java rename to common/src/main/java/com/skyflow/logs/InfoLogs.java index e747bfa2..6d6a8211 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/common/src/main/java/com/skyflow/logs/InfoLogs.java @@ -14,12 +14,14 @@ public enum InfoLogs { // Bearer token generation EMPTY_BEARER_TOKEN("Bearer token is empty."), - BEARER_TOKEN_EXPIRED("Bearer token is invalid or expired."), + BEARER_TOKEN_EXPIRED("Bearer token is expired."), GET_BEARER_TOKEN_TRIGGERED("getBearerToken method triggered."), GET_BEARER_TOKEN_SUCCESS("Bearer token generated."), GET_SIGNED_DATA_TOKENS_TRIGGERED("getSignedDataTokens method triggered."), GET_SIGNED_DATA_TOKEN_SUCCESS("Signed data tokens generated."), REUSE_BEARER_TOKEN("Reusing bearer token."), + USE_CLIENT_PROVIDED_BEARER_TOKEN("Using bearer token provided by client."), + USE_API_KEY("Using api key."), REUSE_API_KEY("Reusing api key."), GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_TRIGGERED("generateBearerTokenFromCredentials method triggered."), GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_STRING_TRIGGERED("generateBearerTokenFromCredentialString method triggered."), @@ -57,6 +59,12 @@ public enum InfoLogs { DELETE_REQUEST_RESOLVED("Delete request resolved."), DELETE_SUCCESS("Data deleted."), + // Delete Tokens interface + DELETE_TOKENS_TRIGGERED("DeleteTokens method triggered."), + VALIDATE_DELETE_TOKENS_REQUEST("Validating delete tokens request."), + DELETE_TOKENS_REQUEST_RESOLVED("DeleteTokens request resolved."), + DELETE_TOKENS_SUCCESS("Tokens deleted."), + // Query interface QUERY_TRIGGERED("Query method triggered."), VALIDATING_QUERY_REQUEST("Validating query request."), @@ -69,11 +77,6 @@ public enum InfoLogs { TOKENIZE_REQUEST_RESOLVED("Tokenize request resolved."), TOKENIZE_SUCCESS("Data tokenized."), - // File upload interface - FILE_UPLOAD_TRIGGERED("File upload method triggered."), - VALIDATING_FILE_UPLOAD_REQUEST("Validating file upload request."), - FILE_UPLOAD_REQUEST_RESOLVED("File upload request resolved."), - FILE_UPLOAD_SUCCESS("File uploaded successfully."), // Invoke connection interface INVOKE_CONNECTION_TRIGGERED("Invoke connection method triggered."), @@ -96,6 +99,14 @@ public enum InfoLogs { VALIDATE_GET_DETECT_RUN_REQUEST("Validating get detect run request."), REIDENTIFY_TEXT_SUCCESS("Text data re-identified."), + // File upload interface + FILE_UPLOAD_TRIGGERED("File upload method triggered."), + VALIDATING_FILE_UPLOAD_REQUEST("Validating file upload request."), + FILE_UPLOAD_REQUEST_RESOLVED("File upload request resolved."), + FILE_UPLOAD_SUCCESS("File uploaded successfully."), + + PROCESSING_BATCHES("Processing batch"), + // Deprecation warnings — v2 backward compat DEPRECATED_SKYFLOW_ID_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), DEPRECATED_SKYFLOW_ID_REQUEST_KEY("[DEPRECATED] Request data key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), @@ -104,8 +115,8 @@ public enum InfoLogs { DEPRECATED_UPDATE_LOG_LEVEL("[DEPRECATED] Method 'updateLogLevel()' is deprecated and will be removed in an upcoming release. Use 'setLogLevel()' instead."), DEPRECATED_CREDENTIAL_CLIENT_ID("[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), DEPRECATED_CREDENTIAL_KEY_ID("[DEPRECATED] Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), - DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."); - + DEPRECATED_CREDENTIAL_TOKEN_URI("[DEPRECATED] Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead.") + ; private final String log; diff --git a/common/src/main/java/com/skyflow/logs/WarningLogs.java b/common/src/main/java/com/skyflow/logs/WarningLogs.java new file mode 100644 index 00000000..1eb1bcd8 --- /dev/null +++ b/common/src/main/java/com/skyflow/logs/WarningLogs.java @@ -0,0 +1,23 @@ +package com.skyflow.logs; + +public enum WarningLogs { + INVALID_BATCH_SIZE_PROVIDED("Invalid value for batch size provided, switching to default value."), + INVALID_CONCURRENCY_LIMIT_PROVIDED("Invalid value for concurrency limit provided, switching to default value."), + BATCH_SIZE_EXCEEDS_MAX_LIMIT("Provided batch size exceeds the maximum limit, switching to max limit."), + CONCURRENCY_EXCEEDS_MAX_LIMIT("Provided concurrency limit exceeds the maximum limit, switching to max limit."), + EMPTY_DELETE_TOKENS_RESPONSE("DeleteTokens response did not include any token results."), + INCOMPLETE_DELETE_TOKENS_RESPONSE("DeleteTokens response did not account for all requested tokens."), + EMPTY_TOKENIZE_RESPONSE("Tokenize response did not include any record results."), + INCOMPLETE_TOKENIZE_RESPONSE("Tokenize response did not account for all requested records.") + ; + + private final String log; + + WarningLogs(String log) { + this.log = log; + } + + public final String getLog() { + return log; + } +} diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/common/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java similarity index 91% rename from src/main/java/com/skyflow/serviceaccount/util/BearerToken.java rename to common/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java index ad7cae30..60c07f6d 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +++ b/common/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java @@ -4,16 +4,16 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; -import com.skyflow.generated.rest.ApiClient; -import com.skyflow.generated.rest.ApiClientBuilder; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.resources.authentication.AuthenticationClient; -import com.skyflow.generated.rest.resources.authentication.requests.V1GetAuthTokenRequest; -import com.skyflow.generated.rest.types.V1GetAuthTokenResponse; +import com.skyflow.generated.auth.rest.ApiClient; +import com.skyflow.generated.auth.rest.ApiClientBuilder; +import com.skyflow.generated.auth.rest.core.ApiClientApiException; +import com.skyflow.generated.auth.rest.resources.authentication.AuthenticationClient; +import com.skyflow.generated.auth.rest.resources.authentication.requests.V1GetAuthTokenRequest; +import com.skyflow.generated.auth.rest.types.V1GetAuthTokenResponse; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; -import com.skyflow.utils.Constants; -import com.skyflow.utils.Utils; +import com.skyflow.utils.BaseConstants; +import com.skyflow.utils.BaseUtils; import com.skyflow.utils.logger.LogUtil; import io.jsonwebtoken.Jwts; @@ -65,13 +65,13 @@ private static V1GetAuthTokenResponse generateBearerTokenFromCredentials( } finally { try { reader.close(); } catch (IOException ignored) {} } - } catch (JsonSyntaxException e) { + } catch (JsonSyntaxException | IllegalStateException e) { LogUtil.printErrorLog(ErrorLogs.INVALID_CREDENTIALS_FILE_FORMAT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString( + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), BaseUtils.parameterizedString( ErrorMessage.FileInvalidJson.getMessage(), credentialsFile.getPath())); } catch (FileNotFoundException e) { LogUtil.printErrorLog(ErrorLogs.CREDENTIALS_FILE_NOT_FOUND.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString( + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), BaseUtils.parameterizedString( ErrorMessage.FileNotFound.getMessage(), credentialsFile.getPath())); } } @@ -87,7 +87,7 @@ private static V1GetAuthTokenResponse generateBearerTokenFromCredentialString( } JsonObject serviceAccountCredentials = JsonParser.parseString(credentials).getAsJsonObject(); return getBearerTokenFromCredentials(serviceAccountCredentials, context, roles); - } catch (JsonSyntaxException e) { + } catch (JsonSyntaxException | IllegalStateException e) { LogUtil.printErrorLog(ErrorLogs.INVALID_CREDENTIALS_STRING_FORMAT.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.CredentialsStringInvalidJson.getMessage()); @@ -141,17 +141,17 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); } - PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); + PrivateKey pvtKey = BaseUtils.getPrivateKeyFromPem(privateKey.getAsString()); String signedUserJWT = getSignedToken( clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context ); - String basePath = Utils.getBaseURL(tokenUri.getAsString()); + String basePath = BaseUtils.getBaseURL(tokenUri.getAsString()); API_CLIENT_BUILDER.url(basePath); ApiClient apiClient = API_CLIENT_BUILDER.token("token").build(); AuthenticationClient authenticationApi = apiClient.authentication(); - V1GetAuthTokenRequest._FinalStage authTokenBuilder = V1GetAuthTokenRequest.builder().grantType(Constants.GRANT_TYPE).assertion(signedUserJWT); + V1GetAuthTokenRequest._FinalStage authTokenBuilder = V1GetAuthTokenRequest.builder().grantType(BaseConstants.GRANT_TYPE).assertion(signedUserJWT); if (roles != null) { String scopedRoles = getScopeUsingRoles(roles); diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java b/common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java similarity index 83% rename from src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java rename to common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java index 3d35b541..c4618ae6 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java +++ b/common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokenResponse.java @@ -1,10 +1,10 @@ package com.skyflow.serviceaccount.util; import com.google.gson.Gson; -import com.skyflow.utils.Constants; +import com.skyflow.utils.BaseConstants; public class SignedDataTokenResponse { - private static final String PREFIX = Constants.SIGNED_DATA_TOKEN_PREFIX; + private static final String PREFIX = BaseConstants.SIGNED_DATA_TOKEN_PREFIX; private final String token; private final String signedToken; diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java similarity index 97% rename from src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java rename to common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java index b909e45b..630a0aee 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +++ b/common/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java @@ -9,7 +9,7 @@ import com.skyflow.errors.SkyflowException; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; -import com.skyflow.utils.Utils; +import com.skyflow.utils.BaseUtils; import com.skyflow.utils.logger.LogUtil; import io.jsonwebtoken.Jwts; @@ -18,11 +18,7 @@ import java.io.FileReader; import java.io.IOException; import java.security.PrivateKey; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; public class SignedDataTokens { private final File credentialsFile; @@ -64,11 +60,11 @@ private static List generateSignedTokenFromCredentialsF } } catch (JsonSyntaxException e) { LogUtil.printErrorLog(ErrorLogs.INVALID_CREDENTIALS_FILE_FORMAT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString( + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), BaseUtils.parameterizedString( ErrorMessage.FileInvalidJson.getMessage(), credentialsFile.getPath())); } catch (FileNotFoundException e) { LogUtil.printErrorLog(ErrorLogs.CREDENTIALS_FILE_NOT_FOUND.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), Utils.parameterizedString( + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), BaseUtils.parameterizedString( ErrorMessage.FileNotFound.getMessage(), credentialsFile.getPath())); } return responseToken; @@ -129,7 +125,7 @@ private static List generateSignedTokensFromCredentials LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); } - PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); + PrivateKey pvtKey = BaseUtils.getPrivateKeyFromPem(privateKey.getAsString()); signedDataTokens = getSignedToken( clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context); } catch (RuntimeException e) { diff --git a/src/main/java/com/skyflow/serviceaccount/util/Token.java b/common/src/main/java/com/skyflow/serviceaccount/util/Token.java similarity index 100% rename from src/main/java/com/skyflow/serviceaccount/util/Token.java rename to common/src/main/java/com/skyflow/serviceaccount/util/Token.java diff --git a/common/src/main/java/com/skyflow/utils/BaseConstants.java b/common/src/main/java/com/skyflow/utils/BaseConstants.java new file mode 100644 index 00000000..1f05fe13 --- /dev/null +++ b/common/src/main/java/com/skyflow/utils/BaseConstants.java @@ -0,0 +1,33 @@ +package com.skyflow.utils; + +public class BaseConstants { + public static final String SDK_NAME= "Skyflow Java SDK "; + public static final String SDK_VERSION = "1.0.0"; + public static final String SDK_PREFIX = SDK_NAME + SDK_VERSION; + public static final String ORDER_ASCENDING = "ASCENDING"; + public static final String ENV_CREDENTIALS_KEY_NAME = "SKYFLOW_CREDENTIALS"; + public static final String SECURE_PROTOCOL = "https://"; + + public static final String V2_VAULT_DOMAIN = ".vault."; + public static final String V3_VAULT_DOMAIN = ".skyvault."; + public static final String DEV_DOMAIN = "skyflowapis.dev"; + public static final String STAGE_DOMAIN = "skyflowapis.tech"; + public static final String SANDBOX_DOMAIN = "skyflowapis-preview.com"; + public static final String PROD_DOMAIN = "skyflowapis.com"; + public static final String PKCS8_PRIVATE_HEADER = "-----BEGIN PRIVATE KEY-----"; + public static final String PKCS8_PRIVATE_FOOTER = "-----END PRIVATE KEY-----"; + public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + public static final String SIGNED_DATA_TOKEN_PREFIX = "signed_token_"; + public static final String API_KEY_REGEX = "^sky-[a-zA-Z0-9]{5}-[a-fA-F0-9]{32}$"; + public static final String CONTEXT_KEY_REGEX = "^[a-zA-Z0-9_]+$"; + public static final String SDK_METRIC_NAME_VERSION = "sdk_name_version"; + public static final String SDK_METRIC_CLIENT_DEVICE_MODEL = "sdk_client_device_model"; + public static final String SDK_METRIC_CLIENT_OS_DETAILS = "sdk_client_os_details"; + public static final String SDK_METRIC_RUNTIME_DETAILS = "sdk_runtime_details"; + public static final String SDK_METRIC_RUNTIME_DETAILS_PREFIX = "Java@"; + public static final String SDK_AUTH_HEADER_KEY = "x-skyflow-authorization"; + public static final String SDK_METRICS_HEADER_KEY = "sky-metadata"; + public static final String REQUEST_ID_HEADER_KEY = "x-request-id"; + public static final String ERROR_FROM_CLIENT_HEADER_KEY = "error-from-client"; + +} diff --git a/src/main/java/com/skyflow/utils/Utils.java b/common/src/main/java/com/skyflow/utils/BaseUtils.java similarity index 63% rename from src/main/java/com/skyflow/utils/Utils.java rename to common/src/main/java/com/skyflow/utils/BaseUtils.java index d116871f..e536c111 100644 --- a/src/main/java/com/skyflow/utils/Utils.java +++ b/common/src/main/java/com/skyflow/utils/BaseUtils.java @@ -1,19 +1,5 @@ package com.skyflow.utils; -import com.google.gson.JsonObject; -import com.skyflow.config.ConnectionConfig; -import com.skyflow.config.Credentials; -import com.skyflow.enums.Env; -import com.skyflow.errors.ErrorCode; -import com.skyflow.errors.ErrorMessage; -import com.skyflow.errors.SkyflowException; -import com.skyflow.logs.ErrorLogs; -import com.skyflow.logs.InfoLogs; -import com.skyflow.serviceaccount.util.BearerToken; -import com.skyflow.utils.logger.LogUtil; -import com.skyflow.vault.connection.InvokeConnectionRequest; -import java.util.Base64; - import java.io.File; import java.net.MalformedURLException; import java.net.URL; @@ -22,33 +8,22 @@ import java.security.PrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.HashMap; +import java.util.Base64; import java.util.Map; -public final class Utils { - public static String getVaultURL(String clusterId, Env env) { - StringBuilder sb = new StringBuilder(Constants.SECURE_PROTOCOL); - sb.append(clusterId); - switch (env) { - case DEV: - sb.append(Constants.DEV_DOMAIN); - break; - case STAGE: - sb.append(Constants.STAGE_DOMAIN); - break; - case SANDBOX: - sb.append(Constants.SANDBOX_DOMAIN); - break; - case PROD: - default: - sb.append(Constants.PROD_DOMAIN); - break; - } - return sb.toString(); - } +import com.google.gson.JsonObject; +import com.skyflow.config.BaseCredentials; +import com.skyflow.enums.Env; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.logs.InfoLogs; +import com.skyflow.serviceaccount.util.BearerToken; +import com.skyflow.utils.logger.LogUtil; - @SuppressWarnings("unchecked") - public static String generateBearerToken(Credentials credentials) throws SkyflowException { +public class BaseUtils { + public static String generateBearerToken(BaseCredentials credentials) throws SkyflowException { if (credentials.getPath() != null) { BearerToken.BearerTokenBuilder builder = BearerToken.builder() .setCredentials(new File(credentials.getPath())) @@ -76,11 +51,29 @@ public static String generateBearerToken(Credentials credentials) throws Skyflow } } + public static String getVaultURL(String clusterId, Env env, String vaultDomain) { + StringBuilder sb = buildBaseUrl(clusterId, vaultDomain); + switch (env) { + case DEV: + sb.append(BaseConstants.DEV_DOMAIN); + break; + case STAGE: + sb.append(BaseConstants.STAGE_DOMAIN); + break; + case SANDBOX: + sb.append(BaseConstants.SANDBOX_DOMAIN); + break; + case PROD: + default: + sb.append(BaseConstants.PROD_DOMAIN); + break; + } + return sb.toString(); + } + public static PrivateKey getPrivateKeyFromPem(String pemKey) throws SkyflowException { - @SuppressWarnings("checkstyle:LocalVariableName") - String PKCS8PrivateHeader = Constants.PKCS8_PRIVATE_HEADER; - @SuppressWarnings("checkstyle:LocalVariableName") - String PKCS8PrivateFooter = Constants.PKCS8_PRIVATE_FOOTER; + String PKCS8PrivateHeader = BaseConstants.PKCS8_PRIVATE_HEADER; + String PKCS8PrivateFooter = BaseConstants.PKCS8_PRIVATE_FOOTER; String privateKeyContent = pemKey; PrivateKey privateKey = null; @@ -88,9 +81,14 @@ public static PrivateKey getPrivateKeyFromPem(String pemKey) throws SkyflowExcep if (pemKey.contains(PKCS8PrivateHeader)) { privateKeyContent = privateKeyContent.replace(PKCS8PrivateHeader, ""); privateKeyContent = privateKeyContent.replace(PKCS8PrivateFooter, ""); - privateKeyContent = privateKeyContent.replace("\n", ""); privateKeyContent = privateKeyContent.replace("\r\n", ""); - privateKey = parsePkcs8PrivateKey(Base64.getDecoder().decode(privateKeyContent)); + privateKeyContent = privateKeyContent.replace("\n", ""); + try { + privateKey = parsePkcs8PrivateKey(Base64.getDecoder().decode(privateKeyContent)); + } catch (IllegalArgumentException e) { + LogUtil.printErrorLog(ErrorLogs.INVALID_KEY_SPEC.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidKeySpec.getMessage()); + } } else { LogUtil.printErrorLog(ErrorLogs.JWT_INVALID_FORMAT.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.JwtInvalidFormat.getMessage()); @@ -112,43 +110,8 @@ public static String parameterizedString(String base, String... args) { return base; } - public static String constructConnectionURL(ConnectionConfig config, InvokeConnectionRequest invokeConnectionRequest) { - StringBuilder filledURL = new StringBuilder(config.getConnectionUrl()); - - if (invokeConnectionRequest.getPathParams() != null && !invokeConnectionRequest.getPathParams().isEmpty()) { - for (Map.Entry entry : invokeConnectionRequest.getPathParams().entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value)); - } - } - - if (invokeConnectionRequest.getQueryParams() != null && !invokeConnectionRequest.getQueryParams().isEmpty()) { - filledURL.append("?"); - for (Map.Entry entry : invokeConnectionRequest.getQueryParams().entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - filledURL.append(key).append("=").append(value).append("&"); - } - filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1)); - } - - return filledURL.toString(); - } - - public static Map constructConnectionHeadersMap(Map requestHeaders) { - Map headersMap = new HashMap<>(); - for (Map.Entry entry : requestHeaders.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - headersMap.put(key.toLowerCase(), value); - } - return headersMap; - } - - public static JsonObject getMetrics() { + protected static JsonObject getCommonMetrics() { JsonObject details = new JsonObject(); - String sdkVersion = Constants.SDK_VERSION; String deviceModel; String osDetails; String javaVersion; @@ -159,7 +122,7 @@ public static JsonObject getMetrics() { } catch (Exception e) { LogUtil.printInfoLog(parameterizedString( InfoLogs.UNABLE_TO_GENERATE_SDK_METRIC.getLog(), - Constants.SDK_METRIC_CLIENT_DEVICE_MODEL + BaseConstants.SDK_METRIC_CLIENT_DEVICE_MODEL )); deviceModel = ""; } @@ -171,7 +134,7 @@ public static JsonObject getMetrics() { } catch (Exception e) { LogUtil.printInfoLog(parameterizedString( InfoLogs.UNABLE_TO_GENERATE_SDK_METRIC.getLog(), - Constants.SDK_METRIC_CLIENT_OS_DETAILS + BaseConstants.SDK_METRIC_CLIENT_OS_DETAILS )); osDetails = ""; } @@ -183,14 +146,13 @@ public static JsonObject getMetrics() { } catch (Exception e) { LogUtil.printInfoLog(parameterizedString( InfoLogs.UNABLE_TO_GENERATE_SDK_METRIC.getLog(), - Constants.SDK_METRIC_RUNTIME_DETAILS + BaseConstants.SDK_METRIC_RUNTIME_DETAILS )); javaVersion = ""; } - details.addProperty(Constants.SDK_METRIC_NAME_VERSION, Constants.SDK_METRIC_NAME_VERSION_PREFIX + sdkVersion); - details.addProperty(Constants.SDK_METRIC_CLIENT_DEVICE_MODEL, deviceModel); - details.addProperty(Constants.SDK_METRIC_RUNTIME_DETAILS, Constants.SDK_METRIC_RUNTIME_DETAILS_PREFIX + javaVersion); - details.addProperty(Constants.SDK_METRIC_CLIENT_OS_DETAILS, osDetails); + details.addProperty(BaseConstants.SDK_METRIC_CLIENT_DEVICE_MODEL, deviceModel); + details.addProperty(BaseConstants.SDK_METRIC_RUNTIME_DETAILS, BaseConstants.SDK_METRIC_RUNTIME_DETAILS_PREFIX + javaVersion); + details.addProperty(BaseConstants.SDK_METRIC_CLIENT_OS_DETAILS, osDetails); return details; } @@ -210,4 +172,11 @@ private static PrivateKey parsePkcs8PrivateKey(byte[] pkcs8Bytes) throws Skyflow } return privateKey; } + + private static StringBuilder buildBaseUrl(String clusterId, String vaultDomain) { + StringBuilder sb = new StringBuilder(BaseConstants.SECURE_PROTOCOL); + sb.append(clusterId); + sb.append(vaultDomain); + return sb; + } } diff --git a/common/src/main/java/com/skyflow/utils/SdkVersion.java b/common/src/main/java/com/skyflow/utils/SdkVersion.java new file mode 100644 index 00000000..8136d8c9 --- /dev/null +++ b/common/src/main/java/com/skyflow/utils/SdkVersion.java @@ -0,0 +1,13 @@ +package com.skyflow.utils; + +public class SdkVersion { + private static String sdkPrefix = BaseConstants.SDK_PREFIX; + + public static String getSdkPrefix() { + return sdkPrefix; + } + + public static void setSdkPrefix(String sdkPrefix) { + SdkVersion.sdkPrefix = sdkPrefix; + } +} diff --git a/common/src/main/java/com/skyflow/utils/logger/LogUtil.java b/common/src/main/java/com/skyflow/utils/logger/LogUtil.java new file mode 100644 index 00000000..ed2b7671 --- /dev/null +++ b/common/src/main/java/com/skyflow/utils/logger/LogUtil.java @@ -0,0 +1,103 @@ +package com.skyflow.utils.logger; + +import com.skyflow.enums.LogLevel; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.SdkVersion; + +import java.util.logging.*; + +public final class LogUtil { + private static final Logger LOGGER = Logger.getLogger(LogUtil.class.getName()); + private static boolean IS_LOGGER_SETUP_DONE = false; + + private static String logPrefix() { + return "[" + SdkVersion.getSdkPrefix() + "] "; + } + + synchronized public static void setupLogger(LogLevel logLevel) { + IS_LOGGER_SETUP_DONE = true; + LogManager.getLogManager().reset(); + LOGGER.setUseParentHandlers(false); + Formatter formatter = new SimpleFormatter() { + private static final String format = "%s: %s %n"; + + // Override format method + @Override + public synchronized String format(LogRecord logRecord) { + return String.format( + format, + loggerLevelToLogLevelMap(logRecord.getLevel()), + logRecord.getMessage() + ); + } + }; + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(formatter); + consoleHandler.setLevel(Level.CONFIG); + + LOGGER.addHandler(consoleHandler); + LOGGER.setLevel(logLevelToLoggerLevelMap(logLevel)); + printInfoLog(InfoLogs.LOGGER_SETUP_DONE.getLog()); + } + + public static void printErrorLog(String message) { + if (IS_LOGGER_SETUP_DONE) + LOGGER.severe(logPrefix() + message); + else { + setupLogger(LogLevel.ERROR); + LOGGER.severe(logPrefix() + message); + } + } + + public static void printDebugLog(String message) { + if (IS_LOGGER_SETUP_DONE) + LOGGER.config(logPrefix() + message); + } + + public static void printWarningLog(String message) { + if (IS_LOGGER_SETUP_DONE) + LOGGER.warning(logPrefix() + message); + } + + public static void printInfoLog(String message) { + if (IS_LOGGER_SETUP_DONE) + LOGGER.info(logPrefix() + message); + } + + + private static Level logLevelToLoggerLevelMap(LogLevel logLevel) { + Level loggerLevel; + switch (logLevel) { + case ERROR: + loggerLevel = Level.SEVERE; + break; + case WARN: + loggerLevel = Level.WARNING; + break; + case INFO: + loggerLevel = Level.INFO; + break; + case DEBUG: + loggerLevel = Level.CONFIG; + break; + default: + loggerLevel = Level.OFF; + } + return loggerLevel; + } + + private static LogLevel loggerLevelToLogLevelMap(Level loggerLevel) { + LogLevel logLevel; + if (Level.SEVERE.equals(loggerLevel)) { + logLevel = LogLevel.ERROR; + } else if (Level.WARNING.equals(loggerLevel)) { + logLevel = LogLevel.WARN; + } else if (Level.INFO.equals(loggerLevel)) { + logLevel = LogLevel.INFO; + } else if (Level.CONFIG.equals(loggerLevel)) { + logLevel = LogLevel.DEBUG; + } else + logLevel = LogLevel.OFF; + return logLevel; + } +} diff --git a/common/src/main/java/com/skyflow/utils/validations/BaseValidations.java b/common/src/main/java/com/skyflow/utils/validations/BaseValidations.java new file mode 100644 index 00000000..c2ee2df9 --- /dev/null +++ b/common/src/main/java/com/skyflow/utils/validations/BaseValidations.java @@ -0,0 +1,111 @@ +package com.skyflow.utils.validations; + +import com.skyflow.config.BaseCredentials; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.utils.BaseConstants; +import com.skyflow.utils.BaseUtils; +import com.skyflow.utils.logger.LogUtil; + +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BaseValidations { + BaseValidations() { + } + + public static void validateCredentials(BaseCredentials credentials) throws SkyflowException { + int nonNullMembers = 0; + String path = credentials.getPath(); + String credentialsString = credentials.getCredentialsString(); + String token = credentials.getToken(); + String apiKey = credentials.getApiKey(); + Object context = credentials.getContext(); + ArrayList roles = credentials.getRoles(); + + if (path != null) nonNullMembers++; + if (credentialsString != null) nonNullMembers++; + if (token != null) nonNullMembers++; + if (apiKey != null) nonNullMembers++; + + if (nonNullMembers > 1) { + LogUtil.printErrorLog(ErrorLogs.MULTIPLE_TOKEN_GENERATION_MEANS_PASSED.getLog()); + throw new SkyflowException( + ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MultipleTokenGenerationMeansPassed.getMessage() + ); + } else if (nonNullMembers < 1) { + LogUtil.printErrorLog(ErrorLogs.NO_TOKEN_GENERATION_MEANS_PASSED.getLog()); + throw new SkyflowException( + ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NoTokenGenerationMeansPassed.getMessage() + ); + } else if (path != null && path.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_PATH.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialFilePath.getMessage()); + } else if (credentialsString != null && credentialsString.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_STRING.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialsString.getMessage()); + } else if (token != null && token.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_TOKEN_VALUE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyToken.getMessage()); + } else if (apiKey != null) { + if (apiKey.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_API_KEY_VALUE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyApikey.getMessage()); + } else { + Pattern pattern = Pattern.compile(BaseConstants.API_KEY_REGEX); + Matcher matcher = pattern.matcher(apiKey); + if (!matcher.matches()) { + LogUtil.printErrorLog(ErrorLogs.INVALID_API_KEY.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidApikey.getMessage()); + } + } + } else if (roles != null) { + if (roles.isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_ROLES.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoles.getMessage()); + } else { + for (int index = 0; index < roles.size(); index++) { + String role = roles.get(index); + if (role == null || role.trim().isEmpty()) { + LogUtil.printErrorLog(BaseUtils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_ROLE_IN_ROLES.getLog(), Integer.toString(index) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoleInRoles.getMessage()); + } + } + } + } + if (context != null) { + if (context instanceof String) { + String ctxStr = (String) context; + if (ctxStr.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } + } else if (context instanceof Map) { + Map ctxMap = (Map) context; + if (ctxMap.isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } + Pattern ctxKeyPattern = Pattern.compile(BaseConstants.CONTEXT_KEY_REGEX); + for (Object key : ctxMap.keySet()) { + if (key == null || !ctxKeyPattern.matcher(key.toString()).matches()) { + String keyStr = key == null ? "null" : key.toString(); + LogUtil.printErrorLog(BaseUtils.parameterizedString( + ErrorLogs.INVALID_CONTEXT_MAP_KEY.getLog(), keyStr)); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + BaseUtils.parameterizedString(ErrorMessage.InvalidContextMapKey.getMessage(), keyStr)); + } + } + } else { + LogUtil.printErrorLog(ErrorLogs.INVALID_CONTEXT_TYPE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidContextType.getMessage()); + } + } + } +} diff --git a/common/src/main/java/com/skyflow/vault/controller/IVaultController.java b/common/src/main/java/com/skyflow/vault/controller/IVaultController.java new file mode 100644 index 00000000..1c4f907f --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/controller/IVaultController.java @@ -0,0 +1,9 @@ +package com.skyflow.vault.controller; + +import com.skyflow.errors.SkyflowException; + +// Common interface — ONLY operations supported on both vault types. +public interface IVaultController { + InsertResp insert(InsertReq request) throws SkyflowException; + DetokenizeResp detokenize(DetokenizeReq request) throws SkyflowException; +} \ No newline at end of file diff --git a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java new file mode 100644 index 00000000..6ff0ee87 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeData.java @@ -0,0 +1,7 @@ +package com.skyflow.vault.data; + +// Shared extension point for module-specific detokenize request data. Intentionally empty: +// v2 and flowvault no longer have any field in common here, so each owns its own state. +// Retained so the modules keep a shared supertype for future use. +public class BaseDetokenizeData { +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRecordResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRecordResponse.java new file mode 100644 index 00000000..7a1ec217 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRecordResponse.java @@ -0,0 +1,19 @@ +package com.skyflow.vault.data; + +public class BaseDetokenizeRecordResponse { + private final String token; + private final String error; + + public BaseDetokenizeRecordResponse(String token, String error){ + this.token = token; + this.error = error; + } + public String getToken() { + return token; + } + + public String getError() { + return error; + } + +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRequest.java b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRequest.java new file mode 100644 index 00000000..d19d0432 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeRequest.java @@ -0,0 +1,4 @@ +package com.skyflow.vault.data; + +public class BaseDetokenizeRequest { +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeResponse.java new file mode 100644 index 00000000..a5061b25 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseDetokenizeResponse.java @@ -0,0 +1,5 @@ +package com.skyflow.vault.data; + +public class BaseDetokenizeResponse { + +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseGetRequest.java b/common/src/main/java/com/skyflow/vault/data/BaseGetRequest.java new file mode 100644 index 00000000..de2ecd12 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseGetRequest.java @@ -0,0 +1,47 @@ +package com.skyflow.vault.data; + +import java.util.ArrayList; + +public class BaseGetRequest { + private final BaseGetRequestBuilder builder; + + protected BaseGetRequest(BaseGetRequestBuilder builder) { + this.builder = builder; + } + + public String getTable() { + return this.builder.table; + } + + public ArrayList getIds() { + return this.builder.ids; + } + + public ArrayList getFields() { + return this.builder.fields; + } + + static class BaseGetRequestBuilder { + protected String table; + protected ArrayList ids; + protected ArrayList fields; + + protected BaseGetRequestBuilder() { + } + + public BaseGetRequestBuilder table(String table) { + this.table = table; + return this; + } + + public BaseGetRequestBuilder ids(ArrayList ids) { + this.ids = ids; + return this; + } + + public BaseGetRequestBuilder fields(ArrayList fields) { + this.fields = fields; + return this; + } + } +} diff --git a/src/main/java/com/skyflow/vault/data/GetResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseGetResponse.java similarity index 65% rename from src/main/java/com/skyflow/vault/data/GetResponse.java rename to common/src/main/java/com/skyflow/vault/data/BaseGetResponse.java index 365fe38e..34545b7f 100644 --- a/src/main/java/com/skyflow/vault/data/GetResponse.java +++ b/common/src/main/java/com/skyflow/vault/data/BaseGetResponse.java @@ -5,11 +5,11 @@ import java.util.ArrayList; import java.util.HashMap; -public class GetResponse { +public class BaseGetResponse { private final ArrayList> data; private final ArrayList> errors; - public GetResponse(ArrayList> data, ArrayList> errors) { + public BaseGetResponse(ArrayList> data, ArrayList> errors) { this.data = data; this.errors = errors; } @@ -17,10 +17,6 @@ public GetResponse(ArrayList> data, ArrayListDeprecation notice: The {@code skyflow_id} key in each record map is - * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. - * Both keys are present simultaneously in v2 for backward compatibility.

*/ public ArrayList> getData() { return data; diff --git a/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java b/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java new file mode 100644 index 00000000..04f82831 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseInsertRequest.java @@ -0,0 +1,7 @@ +package com.skyflow.vault.data; + +// Shared extension point for module-specific insert requests. Intentionally empty: +// v2 and flowvault insert requests no longer have any field in common, so each owns +// its own state. Retained so the modules keep a shared supertype for future use. +public class BaseInsertRequest { +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java new file mode 100644 index 00000000..bb10a688 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseInsertResponse.java @@ -0,0 +1,7 @@ +package com.skyflow.vault.data; + +// Shared extension point for module-specific insert responses. Intentionally empty: +// v2 and flowvault insert responses no longer have any field in common, so each owns +// its own state. Retained so the modules keep a shared supertype for future use. +public class BaseInsertResponse { +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseQueryRequest.java b/common/src/main/java/com/skyflow/vault/data/BaseQueryRequest.java new file mode 100644 index 00000000..01ca3d46 --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseQueryRequest.java @@ -0,0 +1,25 @@ +package com.skyflow.vault.data; + +public class BaseQueryRequest { + private final BaseQueryRequestBuilder builder; + + protected BaseQueryRequest(BaseQueryRequestBuilder builder) { + this.builder = builder; + } + + public String getQuery() { + return this.builder.query; + } + + static class BaseQueryRequestBuilder { + protected String query; + + protected BaseQueryRequestBuilder() { + } + + public BaseQueryRequestBuilder query(String query) { + this.query = query; + return this; + } + } +} diff --git a/common/src/main/java/com/skyflow/vault/data/BaseQueryResponse.java b/common/src/main/java/com/skyflow/vault/data/BaseQueryResponse.java new file mode 100644 index 00000000..1930d66d --- /dev/null +++ b/common/src/main/java/com/skyflow/vault/data/BaseQueryResponse.java @@ -0,0 +1,37 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.HashMap; + +public class BaseQueryResponse { + private final ArrayList> fields; + private final ArrayList> errors; + + public BaseQueryResponse(ArrayList> fields) { + this.fields = fields; + this.errors = null; + } + + /** + * Returns the list of record maps from the Query response. Each map contains all + * field name/value pairs for the record. + */ + public ArrayList> getFields() { + return fields; + } + + /** + * Always returns null. The Query API does not support partial-error responses. + */ + public ArrayList> getErrors() { + return errors; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/common/src/test/java/com/skyflow/BaseSkyflowTests.java b/common/src/test/java/com/skyflow/BaseSkyflowTests.java new file mode 100644 index 00000000..cfd65ad5 --- /dev/null +++ b/common/src/test/java/com/skyflow/BaseSkyflowTests.java @@ -0,0 +1,440 @@ +package com.skyflow; + +import com.skyflow.config.BaseVaultConfig; +import com.skyflow.config.Credentials; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class BaseSkyflowTests { + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; + private static String vaultID = null; + private static String clusterID = null; + private static String newClusterID = null; + private static String token = null; + + @BeforeClass + public static void setup() { + vaultID = "test_vault_id"; + clusterID = "test_cluster_id"; + newClusterID = "new_test_cluster_id"; + token = "test_token"; + } + + private static BaseVaultConfig newConfig(String vaultId, String clusterId, Env env) { + BaseVaultConfig config = new BaseVaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(env); + return config; + } + + @Test + public void testAddingExistingVaultConfigThrows() { + try { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().build(); + client.addVaultConfig(config).addVaultConfig(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdAlreadyInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testUpdatingNonExistentVaultConfigInBuilderThrows() { + try { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow.builder().updateVaultConfig(config).build(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testUpdatingNonExistentVaultConfigInClientThrows() { + try { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().build(); + client.updateVaultConfig(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testRemovingNonExistentVaultConfigInBuilderThrows() { + try { + TestSkyflow.builder().removeVaultConfig(vaultID).build(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testRemovingExistingVaultConfigSucceeds() { + try { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + client.removeVaultConfig(vaultID); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testRemovingVaultConfigWithoutAddingThrows() { + try { + TestSkyflow client = TestSkyflow.builder().build(); + client.removeVaultConfig(vaultID); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testGettingNonExistentVaultConfigReturnsNull() { + TestSkyflow client = TestSkyflow.builder().build(); + Assert.assertNull(client.getVaultConfig(vaultID)); + } + + @Test + public void testVaultThrowsWhenNoConfigAdded() { + try { + TestSkyflow client = TestSkyflow.builder().build(); + client.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testVaultByIdThrowsAfterRemoval() { + try { + BaseVaultConfig primary = newConfig(vaultID, clusterID, Env.SANDBOX); + BaseVaultConfig secondary = newConfig(vaultID + "123", clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(primary).addVaultConfig(secondary).build(); + client.removeVaultConfig(vaultID); + client.vault(vaultID); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); + } + } + + @Test + public void testDefaultLogLevel() { + TestSkyflow client = TestSkyflow.builder().setLogLevel(null).build(); + Assert.assertEquals(LogLevel.ERROR, client.getLogLevel()); + } + + @Test + public void testSetLogLevel() { + TestSkyflow client = TestSkyflow.builder().setLogLevel(LogLevel.INFO).build(); + Assert.assertEquals(LogLevel.INFO, client.getLogLevel()); + client.setLogLevel(LogLevel.WARN); + Assert.assertEquals(LogLevel.WARN, client.getLogLevel()); + } + + @Test + public void testAddingInvalidSkyflowCredentialsThrows() { + try { + Credentials credentials = new Credentials(); + TestSkyflow.builder().addSkyflowCredentials(credentials).build(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.NoTokenGenerationMeansPassed.getMessage(), e.getMessage()); + } + } + + @Test + public void testUpdatingValidSkyflowCredentialsSucceeds() { + try { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + Credentials credentials = new Credentials(); + credentials.setToken(token); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + client.updateSkyflowCredentials(credentials); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testUpdateVaultConfigNullCredentialsFallsBackToPrevious() { + try { + Credentials creds = new Credentials(); + creds.setToken(token); + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + config.setCredentials(creds); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + + BaseVaultConfig partialUpdate = newConfig(vaultID, clusterID, Env.SANDBOX); + client.updateVaultConfig(partialUpdate); + Assert.assertNotNull(client.getVaultConfig(vaultID).getCredentials()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testUpdateVaultConfigWithNewClusterIdAndCredentialsUpdatesAllFields() { + try { + Credentials creds = new Credentials(); + creds.setToken(token); + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.DEV); + config.setCredentials(creds); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + + Credentials newCreds = new Credentials(); + newCreds.setToken("updated-token-value"); + BaseVaultConfig update = newConfig(vaultID, newClusterID, Env.PROD); + update.setCredentials(newCreds); + client.updateVaultConfig(update); + + Assert.assertEquals(newClusterID, client.getVaultConfig(vaultID).getClusterId()); + Assert.assertEquals(Env.PROD, client.getVaultConfig(vaultID).getEnv()); + Assert.assertEquals("updated-token-value", client.getVaultConfig(vaultID).getCredentials().getToken()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testUpdateVaultConfigWithNullEnvFallsBackToPreviousEnv() { + // BaseVaultConfig's setEnv/constructor never store null (default to PROD), so getEnv() + // never returns null via the normal API. Override it to exercise the fallback branch + // in mergeVaultConfig. + try { + Credentials creds = new Credentials(); + creds.setToken(token); + BaseVaultConfig initial = newConfig(vaultID, clusterID, Env.SANDBOX); + initial.setCredentials(creds); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(initial).build(); + + BaseVaultConfig updateWithNullEnv = new BaseVaultConfig() { + @Override + public Env getEnv() { + return null; + } + }; + updateWithNullEnv.setVaultId(vaultID); + updateWithNullEnv.setClusterId(clusterID); + updateWithNullEnv.setCredentials(creds); + + client.updateVaultConfig(updateWithNullEnv); + Assert.assertEquals(Env.SANDBOX, client.getVaultConfig(vaultID).getEnv()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testMergeVaultConfigWithNullClusterIdFallsBackToPreviousClusterId() throws SkyflowException { + BaseVaultConfig existing = newConfig(vaultID, clusterID, Env.DEV); + BaseVaultConfig incoming = new BaseVaultConfig(); + incoming.setVaultId(vaultID); + // clusterId intentionally left null + + BaseVaultConfig result = TestSkyflow.builder().mergeVaultConfig(incoming, existing); + Assert.assertEquals(clusterID, result.getClusterId()); + } + + @Test + public void testVaultReturnsFirstEntryWhenNoVaultIdSpecified() throws SkyflowException { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + + Object vault = client.vault(); + + Assert.assertNotNull(vault); + Assert.assertSame(vault, client.vault(vaultID)); + } + + @Test + public void testVaultByIdReturnsConfigSpecificEntry() throws SkyflowException { + String secondaryId = vaultID + "123"; + BaseVaultConfig primary = newConfig(vaultID, clusterID, Env.SANDBOX); + BaseVaultConfig secondary = newConfig(secondaryId, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(primary).addVaultConfig(secondary).build(); + + Object primaryVault = client.vault(vaultID); + Object secondaryVault = client.vault(secondaryId); + + Assert.assertNotNull(primaryVault); + Assert.assertNotNull(secondaryVault); + Assert.assertNotSame(primaryVault, secondaryVault); + Assert.assertSame(primaryVault, client.vault(vaultID)); + } + + @Test + public void testGettingExistingVaultConfigReturnsStoredConfig() throws SkyflowException { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder().addVaultConfig(config).build(); + + BaseVaultConfig stored = client.getVaultConfig(vaultID); + + Assert.assertNotNull(stored); + Assert.assertEquals(vaultID, stored.getVaultId()); + Assert.assertEquals(clusterID, stored.getClusterId()); + Assert.assertEquals(Env.SANDBOX, stored.getEnv()); + } + + @Test + public void testInstanceSetLogLevelNullResetsToDefault() throws SkyflowException { + TestSkyflow client = TestSkyflow.builder().setLogLevel(LogLevel.INFO).build(); + Assert.assertEquals(LogLevel.INFO, client.getLogLevel()); + + client.setLogLevel(null); + + Assert.assertEquals(LogLevel.ERROR, client.getLogLevel()); + } + + @Test + public void testUpdateVaultConfigLeavesOldConfigWhenHookThrows() throws SkyflowException { + BaseVaultConfig config = newConfig(vaultID, clusterID, Env.SANDBOX); + TestSkyflow client = TestSkyflow.builder() + .addVaultConfig(config) + .failVaultConfigUpdateFor(vaultID) + .build(); + + BaseVaultConfig update = newConfig(vaultID, newClusterID, Env.PROD); + try { + client.updateVaultConfig(update); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + // expected: onVaultConfigUpdated hook forced a failure + } + + BaseVaultConfig stillOld = client.getVaultConfig(vaultID); + Assert.assertEquals(clusterID, stillOld.getClusterId()); + Assert.assertEquals(Env.SANDBOX, stillOld.getEnv()); + } + + private static class TestSkyflow extends BaseSkyflow { + private final TestSkyflowClientBuilder builder; + + private TestSkyflow(TestSkyflowClientBuilder builder) { + super(builder); + this.builder = builder; + } + + static TestSkyflowClientBuilder builder() { + return new TestSkyflowClientBuilder(); + } + + @Override + protected TestSkyflow self() { + return this; + } + + Object vault() throws SkyflowException { + return resolveOrThrow(this.builder.vaultClientsMap, null, + ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); + } + + Object vault(String vaultId) throws SkyflowException { + return resolveOrThrow(this.builder.vaultClientsMap, vaultId, + ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); + } + + private static class TestSkyflowClientBuilder extends BaseSkyflowClientBuilder { + private final java.util.LinkedHashMap vaultClientsMap = new java.util.LinkedHashMap<>(); + private String vaultIdToFailUpdate; + + @Override + protected void validateVaultConfig(BaseVaultConfig vaultConfig) throws SkyflowException { + if (vaultConfig.getVaultId() == null || vaultConfig.getVaultId().trim().isEmpty()) { + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultId.getMessage()); + } + } + + @Override + protected void onVaultConfigAdded(BaseVaultConfig vaultConfig) { + this.vaultClientsMap.put(vaultConfig.getVaultId(), new Object()); + } + + @Override + protected void onVaultConfigUpdated(BaseVaultConfig updatedConfig) throws SkyflowException { + if (updatedConfig.getVaultId().equals(this.vaultIdToFailUpdate)) { + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), "forced update hook failure"); + } + this.vaultClientsMap.put(updatedConfig.getVaultId(), new Object()); + } + + TestSkyflowClientBuilder failVaultConfigUpdateFor(String vaultId) { + this.vaultIdToFailUpdate = vaultId; + return this; + } + + @Override + protected void onVaultConfigRemoved(String vaultId) { + this.vaultClientsMap.remove(vaultId); + } + + @Override + protected boolean hasVaultClient(String vaultId) { + return this.vaultClientsMap.containsKey(vaultId); + } + + @Override + protected void onCredentialsUpdated(Credentials credentials) { + // no-op: this test double only exercises template orchestration, not propagation + } + + @Override + public TestSkyflowClientBuilder addVaultConfig(BaseVaultConfig vaultConfig) throws SkyflowException { + super.addVaultConfig(vaultConfig); + return this; + } + + @Override + public TestSkyflowClientBuilder updateVaultConfig(BaseVaultConfig vaultConfig) throws SkyflowException { + super.updateVaultConfig(vaultConfig); + return this; + } + + @Override + public TestSkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { + super.removeVaultConfig(vaultId); + return this; + } + + @Override + public TestSkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throws SkyflowException { + super.addSkyflowCredentials(credentials); + return this; + } + + @Override + public TestSkyflowClientBuilder setLogLevel(LogLevel logLevel) { + super.setLogLevel(logLevel); + return this; + } + + TestSkyflow build() { + return new TestSkyflow(this); + } + } + } +} diff --git a/common/src/test/java/com/skyflow/BaseVaultClientTests.java b/common/src/test/java/com/skyflow/BaseVaultClientTests.java new file mode 100644 index 00000000..ec78c397 --- /dev/null +++ b/common/src/test/java/com/skyflow/BaseVaultClientTests.java @@ -0,0 +1,304 @@ +package com.skyflow; + +import com.skyflow.config.BaseCredentials; +import com.skyflow.config.BaseVaultConfig; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.utils.BaseConstants; +import okhttp3.Call; +import okhttp3.Connection; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class BaseVaultClientTests { + + private static final String ENV_FILE = ".env"; + private byte[] originalEnvContent; + + @Before + public void saveEnvFileState() throws IOException { + File f = new File(ENV_FILE); + originalEnvContent = f.exists() ? Files.readAllBytes(Paths.get(ENV_FILE)) : null; + } + + @After + public void restoreEnvFile() throws IOException { + if (originalEnvContent != null) { + Files.write(Paths.get(ENV_FILE), originalEnvContent); + } else { + Files.deleteIfExists(Paths.get(ENV_FILE)); + } + } + + private BaseVaultClient newClient(BaseCredentials commonCredentials) { + return new BaseVaultClient<>(new BaseVaultConfig(), commonCredentials); + } + + @Test + public void testPrioritiseCredentials_prefersVaultSpecificCredentials() throws SkyflowException { + BaseCredentials vaultSpecific = new BaseCredentials(); + vaultSpecific.setApiKey("test_api_key"); + BaseVaultClient client = newClient(null); + + client.prioritiseCredentials(vaultSpecific); + + Assert.assertEquals(vaultSpecific, client.finalCredentials); + } + + @Test + public void testPrioritiseCredentials_fallsBackToCommonCredentials() throws SkyflowException { + BaseCredentials common = new BaseCredentials(); + common.setApiKey("common_api_key"); + BaseVaultClient client = newClient(common); + + client.prioritiseCredentials(null); + + Assert.assertEquals(common, client.finalCredentials); + } + + @Test + public void testPrioritiseCredentials_credentialChange_resetsTokenAndApiKey() throws SkyflowException { + BaseCredentials credentialsA = new BaseCredentials(); + credentialsA.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + BaseVaultClient client = newClient(null); + + client.prioritiseCredentials(credentialsA); + client.token = "cached-token"; + client.apiKey = "cached-api-key"; + + BaseCredentials credentialsB = new BaseCredentials(); + credentialsB.setToken("other-token"); + client.prioritiseCredentials(credentialsB); + + Assert.assertNull(client.token); + Assert.assertNull(client.apiKey); + } + + @Test + public void testSetBearerToken_withApiKey() throws SkyflowException { + BaseCredentials creds = new BaseCredentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + BaseVaultClient client = newClient(null); + + client.setBearerToken(creds); + + Assert.assertEquals("sky-ab123-abcd1234cdef1234abcd4321cdef4321", client.token); + } + + @Test + public void testSetBearerToken_generatesTokenWhenNull() throws SkyflowException { + BaseCredentials creds = new BaseCredentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + BaseVaultClient client = newClient(null); + + client.setBearerToken(creds); + + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + } + + @Test + public void testSetBearerToken_reusesValidNonExpiredToken() throws SkyflowException { + BaseCredentials creds = new BaseCredentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + BaseVaultClient client = newClient(null); + + // First call: token=null → generates from creds.getToken() + client.setBearerToken(creds); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + + // Second call: token valid, not expired → reuse branch + client.setBearerToken(creds); + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + } + + @Test + public void testSetBearerToken_noCredentials_throwsEmptyCredentials() { + BaseVaultClient client = newClient(null); + try { + client.setBearerToken(null); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + // message varies by environment (EmptyCredentials when no .env, or credential error when .env provides creds) + } + } + + /** + * Covers the dotenv success path: Dotenv.load() succeeds and returns a + * non-null credentials string, so finalCredentials is set via credentialsString. + */ + @Test + public void testPrioritiseCredentials_dotenvReturnsCredentials_setsCredentials() throws Exception { + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write(BaseConstants.ENV_CREDENTIALS_KEY_NAME + "={\"token\":\"env-token-value\"}\n"); + } + + BaseVaultClient client = newClient(null); + client.prioritiseCredentials(null); + + Assert.assertNotNull(client.finalCredentials); + Assert.assertEquals("{\"token\":\"env-token-value\"}", client.finalCredentials.getCredentialsString()); + } + + /** + * Covers the path where dotenv loads but the key is absent (returns null), + * causing SkyflowException(EmptyCredentials) to be thrown directly. + */ + @Test + public void testPrioritiseCredentials_dotenvKeyMissing_throwsSkyflowException() throws Exception { + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write("SOME_OTHER_KEY=some_value\n"); + } + + BaseVaultClient client = newClient(null); + try { + client.prioritiseCredentials(null); + Assert.fail("Should have thrown SkyflowException"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(ErrorMessage.EmptyCredentials.getMessage())); + } + } + + /** + * Covers buildSharedHttpClient: the interceptor it installs must inject + * "Authorization: Bearer " using the supplied tokenSupplier. No real network call is + * made; a hand-rolled Interceptor.Chain captures the request that would be sent. + */ + @Test + public void testBuildSharedHttpClient_injectsAuthorizationHeaderFromTokenSupplier() throws IOException { + BaseVaultClient client = newClient(null); + Supplier tokenSupplier = () -> "test-shared-token"; + + OkHttpClient httpClient = client.buildSharedHttpClient(tokenSupplier); + + Assert.assertEquals(1, httpClient.interceptors().size()); + Interceptor interceptor = httpClient.interceptors().get(0); + + Request originalRequest = new Request.Builder().url("https://example.com/").build(); + final Request[] capturedRequest = new Request[1]; + Interceptor.Chain fakeChain = new Interceptor.Chain() { + @Override + public Request request() { + return originalRequest; + } + + @Override + public Response proceed(Request request) { + capturedRequest[0] = request; + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .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) { + throw new UnsupportedOperationException(); + } + + @Override + public int readTimeoutMillis() { + return 0; + } + + @Override + public Interceptor.Chain withReadTimeout(int timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeTimeoutMillis() { + return 0; + } + + @Override + public Interceptor.Chain withWriteTimeout(int timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + }; + + interceptor.intercept(fakeChain); + + Assert.assertNotNull(capturedRequest[0]); + Assert.assertEquals("Bearer test-shared-token", capturedRequest[0].header("Authorization")); + } + + /** + * Covers wrapApiException: the returned SkyflowException should carry the given status code, + * and its message should reflect the JSON-serialized responseBody (round-tripped through Gson + * exactly as wrapApiException does internally). + */ + @Test + public void testWrapApiException_carriesStatusCodeAndJsonResponseBody() { + Map errorBody = new HashMap<>(); + errorBody.put("message", "something went wrong"); + Map responseBody = new HashMap<>(); + responseBody.put("error", errorBody); + Map> headers = new HashMap<>(); + + SkyflowException exception = BaseVaultClient.wrapApiException( + 400, new RuntimeException("network error"), headers, responseBody, ErrorLogs.INSERT_RECORDS_REJECTED); + + Assert.assertEquals(400, exception.getHttpCode()); + Assert.assertEquals("something went wrong", exception.getMessage()); + } + + /** + * Covers setBearerToken's "token present but expired -> regenerate" branch. A hand-crafted + * always-expired JWT ("x.eyJleHAiOjF9.y", exp=1 => 1970) is seeded directly into the token + * field, bypassing setBearerToken. The very same credentials instance used to seed + * finalCredentials is then reused when calling setBearerToken, so prioritiseCredentials does + * NOT treat this as a credential change (that branch is covered by + * testPrioritiseCredentials_credentialChange_resetsTokenAndApiKey) - isolating the isExpired + * branch specifically. + */ + @Test + public void testSetBearerToken_expiredToken_regeneratesToken() throws SkyflowException { + BaseCredentials creds = new BaseCredentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); // far-future exp -> not expired once (re)generated + BaseVaultClient client = newClient(null); + + client.prioritiseCredentials(creds); + client.token = "x.eyJleHAiOjF9.y"; // exp=1 (1970) -> always expired, seeded directly + + client.setBearerToken(creds); + + Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", client.token); + Assert.assertNotEquals("x.eyJleHAiOjF9.y", client.token); + } +} diff --git a/common/src/test/java/com/skyflow/config/BaseVaultConfigTests.java b/common/src/test/java/com/skyflow/config/BaseVaultConfigTests.java new file mode 100644 index 00000000..bea158b5 --- /dev/null +++ b/common/src/test/java/com/skyflow/config/BaseVaultConfigTests.java @@ -0,0 +1,75 @@ +package com.skyflow.config; + +import com.skyflow.enums.Env; +import org.junit.Assert; +import org.junit.Test; + +public class BaseVaultConfigTests { + + @Test + public void testDefaultConstructorDefaults() { + BaseVaultConfig config = new BaseVaultConfig(); + Assert.assertNull(config.getVaultId()); + Assert.assertNull(config.getClusterId()); + Assert.assertEquals(Env.PROD, config.getEnv()); + Assert.assertNull(config.getCredentials()); + } + + @Test + public void testGettersAndSetters() { + BaseVaultConfig config = new BaseVaultConfig(); + Credentials credentials = new Credentials(); + credentials.setToken("test_token"); + + config.setVaultId("vault_id"); + config.setClusterId("cluster_id"); + config.setEnv(Env.SANDBOX); + config.setCredentials(credentials); + + Assert.assertEquals("vault_id", config.getVaultId()); + Assert.assertEquals("cluster_id", config.getClusterId()); + Assert.assertEquals(Env.SANDBOX, config.getEnv()); + Assert.assertEquals(credentials, config.getCredentials()); + } + + @Test + public void testSetEnvNullFallsBackToProd() { + BaseVaultConfig config = new BaseVaultConfig(); + config.setEnv(Env.DEV); + config.setEnv(null); + Assert.assertEquals(Env.PROD, config.getEnv()); + } + + @Test + public void testCloneProducesIndependentCopy() throws CloneNotSupportedException { + BaseVaultConfig original = new BaseVaultConfig(); + original.setVaultId("vault_id"); + original.setClusterId("cluster_id"); + original.setEnv(Env.STAGE); + Credentials credentials = new Credentials(); + credentials.setToken("original_token"); + original.setCredentials(credentials); + + BaseVaultConfig cloned = (BaseVaultConfig) original.clone(); + + Assert.assertNotSame(original, cloned); + Assert.assertEquals(original.getVaultId(), cloned.getVaultId()); + Assert.assertEquals(original.getClusterId(), cloned.getClusterId()); + Assert.assertEquals(original.getEnv(), cloned.getEnv()); + Assert.assertNotSame(original.getCredentials(), cloned.getCredentials()); + Assert.assertEquals("original_token", cloned.getCredentials().getToken()); + + // Mutating the clone's credentials must not affect the original + cloned.getCredentials().setToken("mutated_token"); + Assert.assertEquals("original_token", original.getCredentials().getToken()); + } + + @Test + public void testCloneWithNullCredentials() throws CloneNotSupportedException { + BaseVaultConfig original = new BaseVaultConfig(); + original.setVaultId("vault_id"); + + BaseVaultConfig cloned = (BaseVaultConfig) original.clone(); + Assert.assertNull(cloned.getCredentials()); + } +} diff --git a/src/test/java/com/skyflow/config/CredentialsTests.java b/common/src/test/java/com/skyflow/config/CredentialsTests.java similarity index 89% rename from src/test/java/com/skyflow/config/CredentialsTests.java rename to common/src/test/java/com/skyflow/config/CredentialsTests.java index a9a9153f..cdf7fc3f 100644 --- a/src/test/java/com/skyflow/config/CredentialsTests.java +++ b/common/src/test/java/com/skyflow/config/CredentialsTests.java @@ -3,7 +3,7 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; -import com.skyflow.utils.validations.Validations; +import com.skyflow.utils.validations.BaseValidations; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -13,8 +13,6 @@ import java.util.HashMap; import java.util.Map; -import com.skyflow.utils.Utils; - public class CredentialsTests { private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -49,7 +47,7 @@ public void testValidCredentialsWithPath() { try { Credentials credentials = new Credentials(); credentials.setPath(path); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.assertNull(credentials.getCredentialsString()); Assert.assertNull(credentials.getToken()); Assert.assertNull(credentials.getApiKey()); @@ -63,7 +61,7 @@ public void testValidCredentialsWithCredentialsString() { try { Credentials credentials = new Credentials(); credentials.setCredentialsString(credentialsString); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.assertNull(credentials.getPath()); Assert.assertNull(credentials.getToken()); Assert.assertNull(credentials.getApiKey()); @@ -77,7 +75,7 @@ public void testValidCredentialsWithToken() { try { Credentials credentials = new Credentials(); credentials.setToken(token); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.assertNull(credentials.getPath()); Assert.assertNull(credentials.getCredentialsString()); Assert.assertNull(credentials.getApiKey()); @@ -91,7 +89,7 @@ public void testValidCredentialsWithApikey() { try { Credentials credentials = new Credentials(); credentials.setApiKey(validApiKey); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.assertNull(credentials.getPath()); Assert.assertNull(credentials.getCredentialsString()); Assert.assertNull(credentials.getToken()); @@ -108,7 +106,7 @@ public void testValidCredentialsWithRolesAndContext() { credentials.setApiKey(validApiKey); credentials.setRoles(roles); credentials.setContext(context); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.assertNull(credentials.getPath()); Assert.assertNull(credentials.getCredentialsString()); Assert.assertNull(credentials.getToken()); @@ -124,7 +122,7 @@ public void testEmptyPathInCredentials() { try { Credentials credentials = new Credentials(); credentials.setPath(""); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -137,7 +135,7 @@ public void testEmptyCredentialsStringInCredentials() { try { Credentials credentials = new Credentials(); credentials.setCredentialsString(""); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -150,7 +148,7 @@ public void testEmptyTokenInCredentials() { try { Credentials credentials = new Credentials(); credentials.setToken(""); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -163,7 +161,7 @@ public void testEmptyApikeyInCredentials() { try { Credentials credentials = new Credentials(); credentials.setApiKey(""); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -176,7 +174,7 @@ public void testInvalidApikeyInCredentials() { try { Credentials credentials = new Credentials(); credentials.setApiKey(invalidApiKey); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -190,7 +188,7 @@ public void testBothTokenAndPathInCredentials() { Credentials credentials = new Credentials(); credentials.setPath(path); credentials.setToken(token); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -202,7 +200,7 @@ public void testBothTokenAndPathInCredentials() { public void testNothingPassedInCredentials() { try { Credentials credentials = new Credentials(); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -216,7 +214,7 @@ public void testEmptyRolesInCredentials() { Credentials credentials = new Credentials(); credentials.setPath(path); credentials.setRoles(roles); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -232,7 +230,7 @@ public void testNullRoleInRolesInCredentials() { roles.add(role); roles.add(null); credentials.setRoles(roles); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -248,7 +246,7 @@ public void testEmptyRoleInRolesInCredentials() { roles.add(role); roles.add(""); credentials.setRoles(roles); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -262,7 +260,7 @@ public void testEmptyContextInCredentials() { Credentials credentials = new Credentials(); credentials.setPath(path); credentials.setContext(""); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -280,7 +278,7 @@ public void testValidMapContextInCredentials() { ctxMap.put("department", "finance"); ctxMap.put("user_id", "user_12345"); credentials.setContext(ctxMap); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } @@ -293,7 +291,7 @@ public void testEmptyMapContextInCredentials() { credentials.setPath(path); Map ctxMap = new HashMap<>(); credentials.setContext(ctxMap); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -310,7 +308,7 @@ public void testInvalidMapKeyInContextCredentials() { ctxMap.put("valid_key", "value"); ctxMap.put("invalid-key", "value"); credentials.setContext(ctxMap); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); @@ -329,7 +327,7 @@ public void testMapContextWithNestedObjects() { ctxMap.put("role", "admin"); ctxMap.put("metadata", nested); credentials.setContext(ctxMap); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } @@ -346,7 +344,7 @@ public void testMapContextWithMixedValueTypes() { ctxMap.put("active", true); ctxMap.put("timestamp", "2025-12-25T10:30:00Z"); credentials.setContext(ctxMap); - Validations.validateCredentials(credentials); + BaseValidations.validateCredentials(credentials); } catch (SkyflowException e) { Assert.fail(INVALID_EXCEPTION_THROWN); } diff --git a/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java b/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java similarity index 94% rename from src/test/java/com/skyflow/errors/SkyflowExceptionTest.java rename to common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java index 83df09ee..1fb01e6e 100644 --- a/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java +++ b/common/src/test/java/com/skyflow/errors/SkyflowExceptionTest.java @@ -138,6 +138,15 @@ public void testToStringWithNullFields() { Assert.assertTrue(str.contains("details: null")); } + // Regression: skyvault used to ship its own copy of this class whose getHttpCode() + // unboxed the null Integer and threw NPE for every constructor that takes no code. + @Test + public void testGetHttpCodeIsZeroWhenNoCodeWasSet() { + Assert.assertEquals(0, new SkyflowException("local failure").getHttpCode()); + Assert.assertEquals(0, new SkyflowException(new RuntimeException("boom")).getHttpCode()); + Assert.assertEquals(0, new SkyflowException("local failure", new RuntimeException("boom")).getHttpCode()); + } + @Test public void testZeroHttpCodeDefaultsTo400() { Map> headers = new HashMap<>(); diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/common/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java similarity index 76% rename from src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java rename to common/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java index 3aeca811..fb5a2c32 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +++ b/common/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java @@ -1,16 +1,23 @@ package com.skyflow.serviceaccount.util; +import com.google.gson.Gson; +import com.google.gson.JsonObject; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; -import com.skyflow.utils.Constants; -import com.skyflow.utils.Utils; +import com.skyflow.generated.auth.rest.core.ApiClientException; +import com.skyflow.utils.BaseConstants; +import com.skyflow.utils.BaseUtils; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.Map; @@ -92,7 +99,7 @@ public void testEmptyCredentialsFilePath() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), ""), e.getMessage() + BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), ""), e.getMessage() ); } } @@ -107,7 +114,7 @@ public void testInvalidFilePath() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), invalidFilePath), + BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), invalidFilePath), e.getMessage() ); } @@ -123,7 +130,7 @@ public void testInvalidCredentialsFile() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileInvalidJson.getMessage(), invalidJsonFilePath), + BaseUtils.parameterizedString(ErrorMessage.FileInvalidJson.getMessage(), invalidJsonFilePath), e.getMessage() ); } @@ -264,8 +271,58 @@ public void testBearerTokenWithNewFormCredentialKeys() { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); // InvalidKeySpec confirms all credential fields were resolved — failure is at RSA parsing, not field lookup Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.InvalidKeySpec.getMessage(), Constants.SDK_PREFIX), + BaseUtils.parameterizedString(ErrorMessage.InvalidKeySpec.getMessage(), BaseConstants.SDK_PREFIX), e.getMessage()); } } + + /** + * Generates a real, valid PKCS#8-encoded RSA private key PEM string, using the same + * header/footer BaseUtils.getPrivateKeyFromPem expects, so tests can exercise JWT + * signing (getSignedToken()) instead of always failing at key parsing. + */ + private static String generateValidPkcs8PrivateKeyPem() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + String base64EncodedKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); + return BaseConstants.PKCS8_PRIVATE_HEADER + "\n" + base64EncodedKey + "\n" + BaseConstants.PKCS8_PRIVATE_FOOTER; + } + + @Test + public void testGetSignedTokenAndScopeUsingRolesExecuteWithValidKeyRolesAndContext() { + try { + String privateKeyPem = generateValidPkcs8PrivateKeyPem(); + ArrayList testRoles = new ArrayList<>(); + testRoles.add("test_role_one"); + testRoles.add("test_role_two"); + Map ctxMap = new HashMap<>(); + ctxMap.put("role", "admin"); + ctxMap.put("department", "finance"); + + JsonObject credentials = new JsonObject(); + credentials.addProperty("privateKey", privateKeyPem); + credentials.addProperty("clientId", "client_id_value"); + credentials.addProperty("keyId", "key_id_value"); + // Syntactically valid but unreachable, so failure surfaces at the network call, + // proving getSignedToken() (with the ctx claim) and getScopeUsingRoles() (roles != null) + // both executed successfully first. + credentials.addProperty("tokenUri", "https://localhost:1"); + String credentialsString = new Gson().toJson(credentials); + + BearerToken bearerToken = BearerToken.builder() + .setCredentials(credentialsString) + .setCtx(ctxMap) + .setRoles(testRoles) + .build(); + bearerToken.getBearerToken(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (ApiClientException e) { + // Reaching this network-layer exception (rather than a SkyflowException from key + // parsing) confirms getSignedToken() and getScopeUsingRoles() both ran successfully. + Assert.assertTrue(e.getCause() instanceof IOException); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e); + } + } } diff --git a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java b/common/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java similarity index 58% rename from src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java rename to common/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java index 93d69b0e..79542476 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +++ b/common/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java @@ -1,16 +1,25 @@ package com.skyflow.serviceaccount.util; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; -import com.skyflow.utils.Utils; +import com.skyflow.utils.BaseConstants; +import com.skyflow.utils.BaseUtils; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; public class SignedDataTokensTests { @@ -85,7 +94,7 @@ public void testEmptyCredentialsFilePath() { Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(Utils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), ""), e.getMessage()); + Assert.assertEquals(BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), ""), e.getMessage()); } } @@ -99,7 +108,7 @@ public void testInvalidFilePath() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), invalidFilePath), + BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), invalidFilePath), e.getMessage()); } } @@ -114,7 +123,7 @@ public void testInvalidCredentialsFile() { } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileInvalidJson.getMessage(), invalidJsonFilePath), + BaseUtils.parameterizedString(ErrorMessage.FileInvalidJson.getMessage(), invalidJsonFilePath), e.getMessage() ); } @@ -128,12 +137,10 @@ public void testEmptyCredentialsString() { Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.InvalidCredentials.getMessage(), invalidJsonFilePath), - e.getMessage() - ); + // InvalidCredentials has no %s1 placeholder, so no extra arg is passed here. + Assert.assertEquals(ErrorMessage.InvalidCredentials.getMessage(), e.getMessage()); } catch (Exception e) { - System.out.println(e); + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e); } } @@ -145,10 +152,8 @@ public void testInvalidCredentialsString() { Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.CredentialsStringInvalidJson.getMessage(), invalidJsonFilePath), - e.getMessage() - ); + // CredentialsStringInvalidJson has no %s1 placeholder, so no extra arg is passed here. + Assert.assertEquals(ErrorMessage.CredentialsStringInvalidJson.getMessage(), e.getMessage()); } } @@ -258,4 +263,118 @@ public void testSignedDataTokenResponse() { Assert.fail(INVALID_EXCEPTION_THROWN); } } + + /** + * Generates a real, valid PKCS#8-encoded RSA private key PEM string, using the same + * header/footer BaseUtils.getPrivateKeyFromPem expects, so tests can exercise the actual + * JWT signing path (getSignedToken()) instead of always failing at key parsing. + */ + private static String generateValidPkcs8PrivateKeyPem() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + String base64EncodedKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); + return BaseConstants.PKCS8_PRIVATE_HEADER + "\n" + base64EncodedKey + "\n" + BaseConstants.PKCS8_PRIVATE_FOOTER; + } + + /** + * Decodes (without verifying) the payload segment of a compact JWT so tests can assert + * on its claims. These tokens are self-signed with a key generated in-test, so decoding + * the payload directly is sufficient to verify claim content. + */ + private static JsonObject decodeJwtPayload(String jwt) { + String[] parts = jwt.split("\\."); + byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]); + return JsonParser.parseString(new String(payloadBytes, StandardCharsets.UTF_8)).getAsJsonObject(); + } + + @Test + public void testGetSignedDataTokensWithValidKeyMultipleTokensTimeToLiveAndContext() { + try { + String privateKeyPem = generateValidPkcs8PrivateKeyPem(); + ArrayList tokens = new ArrayList<>(); + tokens.add("data_token_one"); + tokens.add("data_token_two"); + Map ctxMap = new HashMap<>(); + ctxMap.put("role", "admin"); + ctxMap.put("department", "finance"); + + JsonObject credentials = new JsonObject(); + credentials.addProperty("privateKey", privateKeyPem); + credentials.addProperty("clientId", "client_id_value"); + credentials.addProperty("keyId", "key_id_value"); + String credentialsJsonString = new Gson().toJson(credentials); + + int timeToLiveSeconds = 120; + long beforeCreationEpochSeconds = System.currentTimeMillis() / 1000; + List responses = SignedDataTokens.builder() + .setCredentials(credentialsJsonString) + .setDataTokens(tokens) + .setTimeToLive(timeToLiveSeconds) + .setCtx(ctxMap) + .build() + .getSignedDataTokens(); + + Assert.assertEquals(tokens.size(), responses.size()); + for (int i = 0; i < tokens.size(); i++) { + SignedDataTokenResponse response = responses.get(i); + Assert.assertEquals(tokens.get(i), response.getToken()); + Assert.assertTrue(response.getSignedToken().startsWith(BaseConstants.SIGNED_DATA_TOKEN_PREFIX)); + + String signedJwt = response.getSignedToken().substring(BaseConstants.SIGNED_DATA_TOKEN_PREFIX.length()); + JsonObject payload = decodeJwtPayload(signedJwt); + Assert.assertEquals(tokens.get(i), payload.get("tok").getAsString()); + Assert.assertEquals("key_id_value", payload.get("key").getAsString()); + Assert.assertEquals("client_id_value", payload.get("sub").getAsString()); + Assert.assertEquals("sdk", payload.get("iss").getAsString()); + + Assert.assertTrue(payload.has("ctx")); + Assert.assertEquals("admin", payload.getAsJsonObject("ctx").get("role").getAsString()); + Assert.assertEquals("finance", payload.getAsJsonObject("ctx").get("department").getAsString()); + + long expirationEpochSeconds = payload.get("exp").getAsLong(); + long expectedExpirationEpochSeconds = beforeCreationEpochSeconds + timeToLiveSeconds; + // Small allowance for time elapsed during test execution + Assert.assertTrue(Math.abs(expirationEpochSeconds - expectedExpirationEpochSeconds) <= 5); + } + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e); + } + } + + @Test + public void testGetSignedDataTokensWithValidKeyAndDefaultTimeToLive() { + try { + String privateKeyPem = generateValidPkcs8PrivateKeyPem(); + ArrayList tokens = new ArrayList<>(); + tokens.add("default_ttl_token"); + + JsonObject credentials = new JsonObject(); + credentials.addProperty("privateKey", privateKeyPem); + credentials.addProperty("clientId", "client_id_value"); + credentials.addProperty("keyId", "key_id_value"); + String credentialsJsonString = new Gson().toJson(credentials); + + long beforeCreationEpochSeconds = System.currentTimeMillis() / 1000; + List responses = SignedDataTokens.builder() + .setCredentials(credentialsJsonString) + .setDataTokens(tokens) + // timeToLive intentionally not set, exercising the default-60-second branch + .build() + .getSignedDataTokens(); + + Assert.assertEquals(1, responses.size()); + String signedJwt = responses.get(0).getSignedToken().substring(BaseConstants.SIGNED_DATA_TOKEN_PREFIX.length()); + JsonObject payload = decodeJwtPayload(signedJwt); + Assert.assertEquals(tokens.get(0), payload.get("tok").getAsString()); + // No context was set, so the "ctx" claim branch should be skipped entirely + Assert.assertFalse(payload.has("ctx")); + + long expirationEpochSeconds = payload.get("exp").getAsLong(); + long expectedExpirationEpochSeconds = beforeCreationEpochSeconds + 60; + Assert.assertTrue(Math.abs(expirationEpochSeconds - expectedExpirationEpochSeconds) <= 5); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN + ": " + e); + } + } } diff --git a/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java b/common/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java similarity index 94% rename from src/test/java/com/skyflow/serviceaccount/util/TokenTests.java rename to common/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java index 88887681..cb631071 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java +++ b/common/src/test/java/com/skyflow/serviceaccount/util/TokenTests.java @@ -1,7 +1,7 @@ package com.skyflow.serviceaccount.util; -import com.skyflow.Skyflow; import com.skyflow.enums.LogLevel; +import com.skyflow.utils.logger.LogUtil; import io.github.cdimascio.dotenv.Dotenv; import org.junit.Assert; import org.junit.BeforeClass; @@ -12,7 +12,7 @@ public class TokenTests { @BeforeClass public static void setup() { - Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.DEBUG).build(); + LogUtil.setupLogger(LogLevel.DEBUG); } @Test diff --git a/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java b/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java new file mode 100644 index 00000000..bc1bd216 --- /dev/null +++ b/common/src/test/java/com/skyflow/utils/BaseUtilsTests.java @@ -0,0 +1,244 @@ +package com.skyflow.utils; + +import com.google.gson.JsonObject; +import com.skyflow.config.Credentials; +import com.skyflow.enums.Env; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class BaseUtilsTests { + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; + private static String clusterId = null; + private static String url = null; + private static String filePath = null; + private static String credentialsString = null; + private static String token = null; + private static String context = null; + private static ArrayList roles = null; + + @BeforeClass + public static void setup() { + clusterId = "test_cluster_id"; + url = "https://test-url.com/java/unit/tests"; + filePath = "invalid/file/path/credentials.json"; + credentialsString = "invalid credentials string"; + token = "invalid-token"; + context = "test_context"; + roles = new ArrayList<>(); + roles.add("test_role"); + } + + @Test + public void testGetVaultURLForDev() { + try { + String vaultURL = BaseUtils.getVaultURL(clusterId, Env.DEV, BaseConstants.V2_VAULT_DOMAIN); + String devUrl = "https://test_cluster_id.vault.skyflowapis.dev"; + Assert.assertEquals(devUrl, vaultURL); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetVaultURLForStage() { + try { + String vaultURL = BaseUtils.getVaultURL(clusterId, Env.STAGE, BaseConstants.V2_VAULT_DOMAIN); + String stageUrl = "https://test_cluster_id.vault.skyflowapis.tech"; + Assert.assertEquals(stageUrl, vaultURL); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetVaultURLForSandbox() { + try { + String vaultURL = BaseUtils.getVaultURL(clusterId, Env.SANDBOX, BaseConstants.V2_VAULT_DOMAIN); + String sandboxUrl = "https://test_cluster_id.vault.skyflowapis-preview.com"; + Assert.assertEquals(sandboxUrl, vaultURL); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetVaultURLForProd() { + try { + String vaultURL = BaseUtils.getVaultURL(clusterId, Env.PROD, BaseConstants.V2_VAULT_DOMAIN); + String prodUrl = "https://test_cluster_id.vault.skyflowapis.com"; + Assert.assertEquals(prodUrl, vaultURL); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetBaseURL() { + try { + String baseURL = BaseUtils.getBaseURL(url); + String expected = "https://test-url.com"; + Assert.assertEquals(expected, baseURL); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGenerateBearerTokenWithCredentialsFile() { + try { + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + credentials.setContext(context); + credentials.setRoles(roles); + BaseUtils.generateBearerToken(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), filePath), + e.getMessage() + ); + } + } + + @Test + public void testGenerateBearerTokenWithCredentialsString() { + try { + Credentials credentials = new Credentials(); + credentials.setCredentialsString(credentialsString); + credentials.setContext(context); + credentials.setRoles(roles); + BaseUtils.generateBearerToken(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.CredentialsStringInvalidJson.getMessage(), e.getMessage()); + } + } + + @Test + public void testGenerateBearerTokenWithToken() { + try { + Credentials credentials = new Credentials(); + credentials.setToken(token); + credentials.setContext(context); + credentials.setRoles(roles); + String bearerToken = BaseUtils.generateBearerToken(credentials); + Assert.assertEquals(token, bearerToken); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetBaseURLWithMalformedURL() { + try { + BaseUtils.getBaseURL("not a url"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (MalformedURLException e) { + // expected + } + } + + @Test + public void testGenerateBearerTokenWithCredentialsFileAndMapContext() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("test_key", "test_value"); + Credentials credentials = new Credentials(); + credentials.setPath(filePath); + credentials.setContext(mapContext); + credentials.setRoles(roles); + BaseUtils.generateBearerToken(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (ClassCastException e) { + Assert.fail("Map context should not cause a ClassCastException"); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals( + BaseUtils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), filePath), + e.getMessage() + ); + } + } + + @Test + public void testGenerateBearerTokenWithCredentialsStringAndMapContext() { + try { + Map mapContext = new HashMap<>(); + mapContext.put("test_key", "test_value"); + Credentials credentials = new Credentials(); + credentials.setCredentialsString(credentialsString); + credentials.setContext(mapContext); + credentials.setRoles(roles); + BaseUtils.generateBearerToken(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (ClassCastException e) { + Assert.fail("Map context should not cause a ClassCastException"); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.CredentialsStringInvalidJson.getMessage(), e.getMessage()); + } + } + + @Test + public void testGenerateBearerTokenWithContextNeitherStringNorMap() { + try { + // context is left unset (null), which is neither a String nor a Map -- this should + // simply be skipped rather than throwing any type-related exception. + Credentials credentials = new Credentials(); + credentials.setCredentialsString(credentialsString); + credentials.setRoles(roles); + BaseUtils.generateBearerToken(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (ClassCastException e) { + Assert.fail("Non String/Map context should not cause a ClassCastException"); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.CredentialsStringInvalidJson.getMessage(), e.getMessage()); + } + } + + @Test + public void testGetPrivateKeyFromPemWithMissingHeader() { + try { + BaseUtils.getPrivateKeyFromPem("this-is-not-a-pem-key-at-all"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.JwtInvalidFormat.getMessage(), e.getMessage()); + } + } + + @Test + public void testGetPrivateKeyFromPemWithInvalidBase64Body() { + String malformedPem = "-----BEGIN PRIVATE KEY-----\nnot-valid-base64!!!\n-----END PRIVATE KEY-----"; + try { + BaseUtils.getPrivateKeyFromPem(malformedPem); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (IllegalArgumentException e) { + Assert.fail("Invalid base64 content should be wrapped into a SkyflowException, not thrown raw"); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.InvalidKeySpec.getMessage(), e.getMessage()); + } + } + + @Test + public void testGetCommonMetrics() { + JsonObject metrics = BaseUtils.getCommonMetrics(); + Assert.assertNotNull(metrics.get(BaseConstants.SDK_METRIC_CLIENT_DEVICE_MODEL)); + Assert.assertNotNull(metrics.get(BaseConstants.SDK_METRIC_RUNTIME_DETAILS)); + Assert.assertNotNull(metrics.get(BaseConstants.SDK_METRIC_CLIENT_OS_DETAILS)); + } +} diff --git a/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java b/common/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java similarity index 58% rename from src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java rename to common/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java index 9754687a..11108e4b 100644 --- a/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java +++ b/common/src/test/java/com/skyflow/utils/logger/LogUtilLevelTests.java @@ -96,4 +96,68 @@ public void testInfoLogSuppressedWhenLogLevelIsWarn() { .anyMatch(r -> r.getLevel().equals(Level.INFO)); Assert.assertFalse("INFO log should NOT appear when LogLevel is WARN", infoCaptured); } + + @Test + public void testDebugLogAppearsWhenLogLevelIsDebug() { + LogUtil.setupLogger(LogLevel.DEBUG); + CapturingHandler handler = attachCapture(); + + LogUtil.printDebugLog("debug message"); + + boolean debugCaptured = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.CONFIG) + && r.getMessage().contains("debug message")); + Assert.assertTrue("DEBUG log should appear when LogLevel is DEBUG", debugCaptured); + } + + @Test + public void testDebugLogSuppressedWhenLogLevelIsInfo() { + LogUtil.setupLogger(LogLevel.INFO); + CapturingHandler handler = attachCapture(); + + LogUtil.printDebugLog("suppressed debug message"); + + boolean debugCaptured = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.CONFIG)); + Assert.assertFalse("DEBUG log should NOT appear when LogLevel is INFO", debugCaptured); + } + + @Test + public void testErrorLogAppearsWhenLogLevelIsError() { + LogUtil.setupLogger(LogLevel.ERROR); + CapturingHandler handler = attachCapture(); + + LogUtil.printErrorLog("error message"); + + boolean errorCaptured = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.SEVERE) + && r.getMessage().contains("error message")); + Assert.assertTrue("ERROR log should appear when LogLevel is ERROR", errorCaptured); + } + + @Test + public void testErrorLogAppearsWhenLogLevelIsDebug() { + LogUtil.setupLogger(LogLevel.DEBUG); + CapturingHandler handler = attachCapture(); + + LogUtil.printErrorLog("debug level error message"); + + boolean errorCaptured = handler.records.stream() + .anyMatch(r -> r.getLevel().equals(Level.SEVERE) + && r.getMessage().contains("debug level error message")); + Assert.assertTrue("ERROR log should appear when LogLevel is DEBUG", errorCaptured); + } + + @Test + public void testNoLogsAppearWhenLogLevelIsOff() { + LogUtil.setupLogger(LogLevel.OFF); + CapturingHandler handler = attachCapture(); + + LogUtil.printErrorLog("off error message"); + LogUtil.printWarningLog("off warning message"); + LogUtil.printInfoLog("off info message"); + LogUtil.printDebugLog("off debug message"); + + Assert.assertTrue("No logs should appear when LogLevel is OFF", handler.records.isEmpty()); + } } diff --git a/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java new file mode 100644 index 00000000..ba75144c --- /dev/null +++ b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeDataTests.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseDetokenizeDataTests { + + @Test + public void testInstantiationDoesNotThrow() { + BaseDetokenizeData data = new BaseDetokenizeData(); + + Assert.assertNotNull(data); + } + + @Test + public void testUsableAsExtensionPointForSubclasses() { + // BaseDetokenizeData carries no state of its own; it exists purely so module-specific + // classes (v2's DetokenizeData, flowvault's TokenGroupRedactions) share a supertype. + BaseDetokenizeData data = new BaseDetokenizeData() { + }; + + Assert.assertTrue(data instanceof BaseDetokenizeData); + } +} diff --git a/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRecordResponseTests.java b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRecordResponseTests.java new file mode 100644 index 00000000..52992e46 --- /dev/null +++ b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRecordResponseTests.java @@ -0,0 +1,39 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseDetokenizeRecordResponseTests { + + @Test + public void testGettersReturnConstructorValuesOnSuccess() { + BaseDetokenizeRecordResponse response = new BaseDetokenizeRecordResponse("token-value", null); + + Assert.assertEquals("token-value", response.getToken()); + Assert.assertNull(response.getError()); + } + + @Test + public void testGettersReturnConstructorValuesOnError() { + BaseDetokenizeRecordResponse response = new BaseDetokenizeRecordResponse(null, "some error"); + + Assert.assertNull(response.getToken()); + Assert.assertEquals("some error", response.getError()); + } + + @Test + public void testBothTokenAndErrorNull() { + BaseDetokenizeRecordResponse response = new BaseDetokenizeRecordResponse(null, null); + + Assert.assertNull(response.getToken()); + Assert.assertNull(response.getError()); + } + + @Test + public void testBothTokenAndErrorPopulated() { + BaseDetokenizeRecordResponse response = new BaseDetokenizeRecordResponse("token-value", "some error"); + + Assert.assertEquals("token-value", response.getToken()); + Assert.assertEquals("some error", response.getError()); + } +} diff --git a/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRequestTests.java b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRequestTests.java new file mode 100644 index 00000000..0b414353 --- /dev/null +++ b/common/src/test/java/com/skyflow/vault/data/BaseDetokenizeRequestTests.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseDetokenizeRequestTests { + + @Test + public void testInstantiationDoesNotThrow() { + BaseDetokenizeRequest request = new BaseDetokenizeRequest(); + + Assert.assertNotNull(request); + } + + @Test + public void testUsableAsExtensionPointForSubclasses() { + // BaseDetokenizeRequest carries no state of its own; it exists purely so module-specific + // DetokenizeRequest classes (e.g. flowvault's) can extend it. Verify the subtype relationship holds. + BaseDetokenizeRequest request = new BaseDetokenizeRequest() { + }; + + Assert.assertTrue(request instanceof BaseDetokenizeRequest); + } +} diff --git a/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java b/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java new file mode 100644 index 00000000..5b59e261 --- /dev/null +++ b/common/src/test/java/com/skyflow/vault/data/BaseInsertRequestTests.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseInsertRequestTests { + + @Test + public void testInstantiationDoesNotThrow() { + BaseInsertRequest request = new BaseInsertRequest(); + + Assert.assertNotNull(request); + } + + @Test + public void testUsableAsExtensionPointForSubclasses() { + // BaseInsertRequest carries no state of its own; it exists purely so module-specific + // InsertRequest classes (v2's and flowvault's) share a supertype. Verify the subtype relationship holds. + BaseInsertRequest request = new BaseInsertRequest() { + }; + + Assert.assertTrue(request instanceof BaseInsertRequest); + } +} diff --git a/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java b/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java new file mode 100644 index 00000000..182d0d05 --- /dev/null +++ b/common/src/test/java/com/skyflow/vault/data/BaseInsertResponseTests.java @@ -0,0 +1,24 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseInsertResponseTests { + + @Test + public void testInstantiationDoesNotThrow() { + BaseInsertResponse response = new BaseInsertResponse(); + + Assert.assertNotNull(response); + } + + @Test + public void testUsableAsExtensionPointForSubclasses() { + // BaseInsertResponse carries no state of its own; it exists purely so module-specific + // InsertResponse classes (v2's and flowvault's) share a supertype. Verify the subtype relationship holds. + BaseInsertResponse response = new BaseInsertResponse() { + }; + + Assert.assertTrue(response instanceof BaseInsertResponse); + } +} diff --git a/src/test/resources/invalidPrivateKeyCredentials.json b/common/src/test/resources/invalidPrivateKeyCredentials.json similarity index 100% rename from src/test/resources/invalidPrivateKeyCredentials.json rename to common/src/test/resources/invalidPrivateKeyCredentials.json diff --git a/src/test/resources/invalidTokenURICredentials.json b/common/src/test/resources/invalidTokenURICredentials.json similarity index 100% rename from src/test/resources/invalidTokenURICredentials.json rename to common/src/test/resources/invalidTokenURICredentials.json diff --git a/src/test/resources/noClientIDCredentials.json b/common/src/test/resources/noClientIDCredentials.json similarity index 100% rename from src/test/resources/noClientIDCredentials.json rename to common/src/test/resources/noClientIDCredentials.json diff --git a/src/test/resources/noKeyIDCredentials.json b/common/src/test/resources/noKeyIDCredentials.json similarity index 100% rename from src/test/resources/noKeyIDCredentials.json rename to common/src/test/resources/noKeyIDCredentials.json diff --git a/src/test/resources/noPrivateKeyCredentials.json b/common/src/test/resources/noPrivateKeyCredentials.json similarity index 100% rename from src/test/resources/noPrivateKeyCredentials.json rename to common/src/test/resources/noPrivateKeyCredentials.json diff --git a/src/test/resources/noTokenURICredentials.json b/common/src/test/resources/noTokenURICredentials.json similarity index 100% rename from src/test/resources/noTokenURICredentials.json rename to common/src/test/resources/noTokenURICredentials.json diff --git a/src/test/resources/notJson.txt b/common/src/test/resources/notJson.txt similarity index 100% rename from src/test/resources/notJson.txt rename to common/src/test/resources/notJson.txt diff --git a/flowvault/README.md b/flowvault/README.md new file mode 100644 index 00000000..442a1fbf --- /dev/null +++ b/flowvault/README.md @@ -0,0 +1,845 @@ +# Skyflow FlowVault Java SDK + +The `flowvault` module is a Skyflow Java SDK built for high-throughput vault operations. It shares its client, credentials, and configuration classes with the [skyvault SDK](../skyvault/README.md) (both depend on the `common` module) but exposes a different, narrower surface: **bulk** vault operations only. + +> Meant for **Flow DB** vaults. + +> **`flowvault` is a new SDK, versioned independently of `skyvault`.** It starts at `1.0.0` while `skyvault` (`com.skyflow:skyflow-java`) is at `2.x`. The two artifacts have separate version lines, so a lower `flowvault` version number does not mean it is older or behind — it is a first release, not a downgrade. Upgrade each artifact on its own. + +[![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-java)](https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE) + +# Table of Contents + +- [Table of Contents](#table-of-contents) +- [Overview](#overview) +- [Install](#install) + - [Requirements](#requirements) + - [Configuration](#configuration) +- [Quickstart](#quickstart) +- [Authenticate](#authenticate) + - [Credential types](#credential-types) + - [Where credentials can be set](#where-credentials-can-be-set) + - [Generate a bearer token](#generate-a-bearer-token) + - [Context-aware and scoped tokens](#context-aware-and-scoped-tokens) +- [Initialize the client](#initialize-the-client) + - [VaultConfig reference](#vaultconfig-reference) + - [Skyflow.builder() reference](#skyflowbuilder-reference) + - [Timeouts and retries](#timeouts-and-retries) + - [Logging](#logging) +- [VaultController — Bulk operations](#vaultcontroller--bulk-operations) + - [Batching and concurrency](#batching-and-concurrency) +- [Bulk Insert](#bulk-insert) +- [Bulk Tokenize](#bulk-tokenize) +- [Bulk Detokenize](#bulk-detokenize) +- [Bulk Delete Tokens](#bulk-delete-tokens) +- [Custom Request Headers](#custom-request-headers) +- [Error Handling](#error-handling) + - [Two layers of errors](#two-layers-of-errors) + - [Per-record success and failure](#per-record-success-and-failure) + - [Catching SkyflowException](#catching-skyflowexception) + - [SkyflowException properties](#skyflowexception-properties) + - [Retrying the failed records](#retrying-the-failed-records) + +# Overview + +- Authenticate using a Skyflow service account, an API key, or a bearer token — see [Authenticate](#authenticate). +- Perform bulk Vault API operations — insert, tokenize, detokenize, and delete tokens — each with a synchronous and an async variant, built for high-throughput Flow DB workloads. +- **Per-record reporting, not all-or-nothing.** A bulk call succeeds as a call even when individual records fail; every response reports a summary plus the outcome of each individual record or token. See [Error Handling](#error-handling). + +# Install + +## Requirements + +- Java 8 and above + +## Configuration + +### Gradle users + +``` +implementation 'com.skyflow:skyflow-flowvault-java:1.0.0' +``` + +### Maven users + +```xml + + com.skyflow + skyflow-flowvault-java + 1.0.0 + +``` + +# Quickstart + +```java +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.vault.controller.VaultController; + +Credentials credentials = new Credentials(); +credentials.setApiKey(""); // or setToken / setCredentialsString / setPath + +VaultConfig vaultConfig = new VaultConfig(); +vaultConfig.setVaultId(""); +vaultConfig.setClusterId(""); // part of the vault URL, e.g. https://{clusterId}.vault.skyflowapis.com +vaultConfig.setEnv(Env.PROD); +vaultConfig.setCredentials(credentials); + +Skyflow skyflowClient = Skyflow.builder() + .addVaultConfig(vaultConfig) + .build(); + +// Returns the controller for the first configured vault +VaultController vault = skyflowClient.vault(); +``` + +`flowvault`'s `vault()` takes no arguments — it always resolves to the first vault added to the builder. Use one client per vault if you need to talk to more than one. + +# Authenticate + +Requests are authorized with Skyflow credentials that you attach to a `Credentials` object. `Credentials` comes from the shared `common` module, so it is the same class `skyvault` uses. + +## Credential types + +Set exactly one of the following on a `Credentials` instance. If you set more than one, **the last one set wins**. + +| Credential | Setter | What it is | +|---|---|---| +| API key | `setApiKey(String)` | A long-lived key that authenticates and authorizes requests to the API. Simplest option. | +| Bearer token | `setToken(String)` | A short-lived access token, typically generated from service account credentials. See [Generate a bearer token](#generate-a-bearer-token). | +| Credentials file path | `setPath(String)` | Filesystem path to a service account `credentials.json`. The SDK generates and refreshes bearer tokens from it. | +| Credentials string | `setCredentialsString(String)` | The contents of a service account `credentials.json` as a JSON string — use this when the credentials come from a secret store rather than a file. | + +Two optional modifiers apply when the SDK is generating tokens for you (that is, with `setPath` or `setCredentialsString`): + +| Setter | Description | +|---|---| +| `setRoles(ArrayList)` | Restrict the generated token to specific role IDs (a scoped token). | +| `setContext(String)` / `setContext(Map)` | Attach context to the generated token for context-aware authorization. | + +```java +// API key +Credentials apiKeyCredentials = new Credentials(); +apiKeyCredentials.setApiKey(""); + +// Bearer token you generated yourself +Credentials tokenCredentials = new Credentials(); +tokenCredentials.setToken(""); + +// Service account credentials file — the SDK handles token generation and refresh +Credentials fileCredentials = new Credentials(); +fileCredentials.setPath(""); + +// Service account credentials as a JSON string +Credentials stringCredentials = new Credentials(); +stringCredentials.setCredentialsString(""); +``` + +## Where credentials can be set + +Credentials resolve **most specific first**: + +1. **Per-vault** — `vaultConfig.setCredentials(credentials)`. Wins for that vault. +2. **Client-wide** — `Skyflow.builder().addSkyflowCredentials(credentials)`. Used by any vault that has none of its own. +3. **Environment** — if neither is provided, the SDK reads the `SKYFLOW_CREDENTIALS` environment variable. + +If none of the three yields credentials, the call fails with a `SkyflowException`. + +## Generate a bearer token + +If you would rather manage tokens yourself, `common` ships the same `BearerToken` utility as `skyvault`: + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; + +BearerToken token = BearerToken.builder() + .setCredentials(new File("")) // or setCredentials(credentialsJsonString) + .build(); + +String bearerToken = token.getBearerToken(); // cached and regenerated only when expired + +Credentials credentials = new Credentials(); +credentials.setToken(bearerToken); +``` + +`getBearerToken()` caches the token and only mints a new one once the current one has expired, so it is safe to call per request. + +## Context-aware and scoped tokens + +`BearerToken.builder()` also accepts `setCtx(String | Map)` for context-aware authorization and `setRoles(ArrayList)` for scoped tokens. Signed data tokens are available through `com.skyflow.serviceaccount.util.SignedDataTokens`. These utilities are identical to skyvault's — see [Authenticate with bearer tokens](../skyvault/README.md#authenticate-with-bearer-tokens) for worked examples of every variant. + +# Initialize the client + +`Skyflow` is the client. Build it once, keep it for the lifetime of your application, and get a `VaultController` from it with `vault()`. + +```java +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.controller.VaultController; + +public class InitFlowVaultClient { + public static void main(String[] args) throws SkyflowException { + // Step 1: Credentials — exactly one credential type + Credentials credentials = new Credentials(); + credentials.setPath(""); + + // Step 2: Vault configuration + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); + vaultConfig.setClusterId(""); + vaultConfig.setEnv(Env.PROD); // DEV, STAGE, SANDBOX, or PROD (default) + vaultConfig.setCredentials(credentials); + + // Optional: vault-level HTTP overrides + vaultConfig.setTimeout(120); // overall call timeout, in seconds + vaultConfig.setMaxRetries(2); // retries after the first failure + + // Step 3: Build the client + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.INFO) // default is ERROR + .addVaultConfig(vaultConfig) + .build(); + + // Step 4: Get the controller and issue bulk calls + VaultController vault = skyflowClient.vault(); + } +} +``` + +## VaultConfig reference + +| Setter | Type | Description | +|---|---|---| +| `setVaultId(String)` | required | The vault's ID. | +| `setClusterId(String)` | required | The cluster portion of the vault URL — `https://{clusterId}.vault.skyflowapis.com`. | +| `setEnv(Env)` | optional | `Env.DEV`, `Env.STAGE`, `Env.SANDBOX`, or `Env.PROD`. Defaults to `PROD`; passing `null` also resolves to `PROD`. | +| `setCredentials(Credentials)` | optional | Credentials for this vault. Falls back to client-wide credentials, then `SKYFLOW_CREDENTIALS`. | +| `setVaultUrl(String)` | optional | Full vault URL, when it cannot be derived from `clusterId` and `env`. | +| `setTimeout(Integer)` | optional | Overall call timeout in seconds, including retries. | +| `setConnectTimeout(Integer)` | optional | Per-attempt connection timeout, in seconds. | +| `setReadTimeout(Integer)` | optional | Per-attempt response-read timeout, in seconds. | +| `setWriteTimeout(Integer)` | optional | Per-attempt request-write timeout, in seconds. | +| `setMaxRetries(Integer)` | optional | Retry attempts after the first failure. | +| `setInitialRetryDelayMillis(Long)` | optional | Backoff before the first retry, in milliseconds. | +| `setMaxRetryDelayMillis(Long)` | optional | Ceiling the exponential backoff grows to, in milliseconds. | + +## Skyflow.builder() reference + +| Method | Description | +|---|---| +| `addVaultConfig(VaultConfig)` | Register a vault. The first one registered is what `vault()` returns. | +| `updateVaultConfig(VaultConfig)` | Update a registered vault in place. `null` fields mean "leave as is". | +| `removeVaultConfig(String vaultId)` | Unregister a vault. | +| `addSkyflowCredentials(Credentials)` | Client-wide credentials for vaults that don't set their own. | +| `setLogLevel(LogLevel)` | `DEBUG`, `INFO`, `WARN`, `ERROR` (default), or `OFF`. | +| `timeout(int)` / `connectTimeout(int)` / `readTimeout(int)` / `writeTimeout(int)` | Client-wide HTTP timeouts, in seconds. | +| `maxRetries(int)` / `initialRetryDelayMillis(long)` / `maxRetryDelayMillis(long)` | Client-wide retry policy. | +| `build()` | Produce the `Skyflow` client. | + +Every method throws `SkyflowException` on validation errors and returns the builder for chaining. + +## Timeouts and retries + +Each HTTP setting resolves **most specific first**: the value on `VaultConfig`, else the client-wide value on `Skyflow.builder()`, else the SDK default. Only `null` means "inherit" — an explicit `0` is a real value and overrides the level below it. + +| Setting | SDK default | +|---|---| +| `timeout` (overall call, incl. retries) | 60 s | +| `connectTimeout` / `readTimeout` / `writeTimeout` (per attempt) | 10 s (underlying HTTP client default) | +| `maxRetries` | `0` — retries are **opt-in**, so non-idempotent bulk writes are never replayed silently | +| `initialRetryDelayMillis` | 500 ms | +| `maxRetryDelayMillis` | 2000 ms | + +```java +// Client-wide policy, overridden for one vault +VaultConfig vaultConfig = new VaultConfig(); +vaultConfig.setVaultId(""); +vaultConfig.setClusterId(""); +vaultConfig.setCredentials(credentials); +vaultConfig.setTimeout(300); // this vault gets 300s... + +Skyflow skyflowClient = Skyflow.builder() + .timeout(60) // ...instead of the client-wide 60s + .maxRetries(3) // this vault inherits 3 retries + .initialRetryDelayMillis(500L) + .maxRetryDelayMillis(4000L) + .addVaultConfig(vaultConfig) + .build(); +``` + +## Logging + +The SDK logs through `java.util.logging` at `LogLevel.ERROR` by default. Levels rank `DEBUG` < `INFO` < `WARN` < `ERROR` < `OFF`; setting a level prints that level and everything above it. Change it with `Skyflow.builder().setLogLevel(LogLevel.DEBUG)`. + +# VaultController — Bulk operations + +`VaultController` is returned by `skyflowClient.vault()`. `flowvault` exposes these bulk vault operations: + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `bulkInsert(BulkInsertRequest)` | `BulkInsertRequest`, optional `BulkInsertOptions` | `BulkInsertResponse` | Insert many records, optionally across multiple tables, in one call | +| `bulkInsertAsync(BulkInsertRequest)` | same | `CompletableFuture` | Async variant of `bulkInsert` | +| `bulkTokenize(BulkTokenizeRequest)` | `BulkTokenizeRequest`, optional `BulkTokenizeOptions` | `BulkTokenizeResponse` | Tokenize many values, each against one or more named token groups | +| `bulkTokenizeAsync(BulkTokenizeRequest)` | same | `CompletableFuture` | Async variant of `bulkTokenize` | +| `bulkDetokenize(BulkDetokenizeRequest)` | `BulkDetokenizeRequest`, optional `BulkDetokenizeOptions` | `BulkDetokenizeResponse` | Detokenize many tokens, optionally with a redaction override per token group | +| `bulkDetokenizeAsync(BulkDetokenizeRequest)` | same | `CompletableFuture` | Async variant of `bulkDetokenize` | +| `bulkDeleteTokens(BulkDeleteTokensRequest)` | `BulkDeleteTokensRequest`, optional `BulkDeleteTokensOptions` | `BulkDeleteTokensResponse` | Delete many tokens in one call | +| `bulkDeleteTokensAsync(BulkDeleteTokensRequest)` | same | `CompletableFuture` | Async variant of `bulkDeleteTokens` | + +Each method also accepts an optional options object (`BulkInsertOptions`, `BulkTokenizeOptions`, `BulkDetokenizeOptions`, `BulkDeleteTokensOptions`) — see [Custom Request Headers](#custom-request-headers). + +A single bulk call accepts at most **10,000** records or tokens; anything larger is rejected up front with a `SkyflowException`. Under that ceiling the SDK splits the payload into batches and sends them concurrently, which is why errors from one call can carry different `requestId` values. + +Every bulk response has the same two-part shape: + +- a **summary** — totals for the call (e.g. `totalRecords` / `totalInserted` / `totalFailed` for insert) +- a **records** list — one entry per submitted record or token, in input order, each carrying its own `index`, `httpCode`, and `error` + +That per-record shape is the point of these APIs; see [Error Handling](#error-handling) for the full model. + +## Batching and concurrency + +Batch size and concurrency are configured **per operation** through environment variables — there is no builder or options API for them. Each value is read from the process environment first, then from a `.env` file in the working directory. + +| Operation | Batch size variable | Default | Max | Concurrency variable | Default | Max | +|-----------|--------------------|---------|-----|---------------------|---------|-----| +| Bulk insert | `INSERT_BATCH_SIZE` | 50 | 1000 | `INSERT_CONCURRENCY_LIMIT` | 1 | 10 | +| Bulk tokenize | `TOKENIZE_BATCH_SIZE` | 50 | 1000 | `TOKENIZE_CONCURRENCY_LIMIT` | 1 | 10 | +| Bulk detokenize | `DETOKENIZE_BATCH_SIZE` | 50 | 1000 | `DETOKENIZE_CONCURRENCY_LIMIT` | 1 | 10 | +| Bulk delete tokens | `DELETE_TOKENS_BATCH_SIZE` | 50 | 1000 | `DELETE_TOKENS_CONCURRENCY_LIMIT` | 1 | 10 | + +Concurrency defaults to **1**, so batches are sent one after another unless you raise the limit. + +How each value is resolved: + +- **Batch size** — `min(yourValue, max)`. Above the max, the SDK logs a warning and uses the max. Zero, negative, or non-numeric values log a warning and fall back to the default. +- **Concurrency** — `min(yourValue, max, batchCount)`, where `batchCount = ceil(itemCount / batchSize)`. Concurrency never exceeds the number of batches there are to run. Same warning-and-fallback behaviour for invalid values. + +Those warnings are emitted at `WARN`, which the default `ERROR` level hides — set `LogLevel.WARN` or below to see them (see [Logging](#logging)). + +For example, 500 records with `INSERT_BATCH_SIZE=100` and `INSERT_CONCURRENCY_LIMIT=10` produces 5 batches, all 5 in flight at once — the concurrency is capped to 5, not 10. + +```dotenv +# .env +INSERT_BATCH_SIZE=100 +INSERT_CONCURRENCY_LIMIT=5 +``` + +The 10,000-item ceiling per bulk call is a separate, fixed limit and is not configurable. + +# Bulk Insert + +Insert many records — even across different tables — in a single call. Each record is a `BulkInsertRequestRecord` with its own `data` and, optionally, its own `tableName` and `upsert`. + +**Note:** + +- `tableName` must be specified at exactly one level: either on the request (`BulkInsertRequest.builder().tableName(...)`) or on **every** record (`BulkInsertRequestRecord.builder().tableName(...)`) — not both, and not neither. +- `upsert` is optional, but wherever you supply it, it must sit at the same level as `tableName`. Request-level `tableName` pairs with request-level `upsert`; record-level `tableName` pairs with per-record `upsert`. +- `UpsertOptions` requires `uniqueColumns`. `updateType` accepts `"UPDATE"` (the default) or `"REPLACE"`. + +### Construct a bulk insert request + +```java +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.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BulkInsertExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Build each record. Here tableName lives on the records, so each one carries it. + Map record1Data = new HashMap<>(); + record1Data.put("card_number", "4111111111111111"); + record1Data.put("cardholder_name", "john doe"); + + BulkInsertRequestRecord record1 = BulkInsertRequestRecord.builder() + .tableName("table1") + .data(record1Data) + .build(); + + Map record2Data = new HashMap<>(); + record2Data.put("email", "jane.doe@example.com"); + + BulkInsertRequestRecord record2 = BulkInsertRequestRecord.builder() + .tableName("table2") + .data(record2Data) + // upsert sits at the record level here, matching where tableName sits + .upsert(UpsertOptions.builder() + .uniqueColumns(Arrays.asList("email")) + .updateType("UPDATE") + .build()) + .build(); + + List records = new ArrayList<>(); + records.add(record1); + records.add(record2); + + // Step 2: Build the BulkInsertRequest + BulkInsertRequest insertRequest = BulkInsertRequest.builder() + .records(records) + .build(); + + // Step 3: Perform the bulk insert + BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest); + System.out.println(insertResponse); + } +} +``` + +To put the table name on the request instead, drop `tableName` from every record and build the request as: + +```java +BulkInsertRequest insertRequest = BulkInsertRequest.builder() + .tableName("table1") + .upsert(UpsertOptions.builder().uniqueColumns(Arrays.asList("email")).build()) + .records(records) + .build(); +``` + +### Async bulk insert + +```java +import java.util.concurrent.CompletableFuture; + +CompletableFuture future = vault.bulkInsertAsync(insertRequest); +future.thenAccept(response -> System.out.println(response)); +``` + +Sample response: + +```json +{ + "summary": { "totalRecords": 2, "totalInserted": 1, "totalFailed": 1 }, + "records": [ + { + "index": 0, + "requestId": null, + "tableName": "table1", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "fields": { "card_number": "5484-7829-1702-9110", "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" }, + "hashedData": null, + "httpCode": 200, + "error": null + }, + { + "index": 1, + "requestId": "a1b2c3d4-...", + "tableName": "table2", + "skyflowId": null, + "fields": null, + "hashedData": null, + "httpCode": 400, + "error": "Insert failed. Column email is invalid." + } + ] +} +``` + +Accessors: `insertResponse.getSummary()`, `insertResponse.getRecords()`, and on each record `getIndex()`, `getTableName()`, `getSkyflowId()`, `getFields()`, `getHashedData()`, `getHttpCode()`, `getError()`, `getRequestId()`. + +Use `insertResponse.getRecordsToRetry()` to get back only the `BulkInsertRequestRecord`s worth resubmitting — see [Retrying the failed records](#retrying-the-failed-records). + +# Bulk Tokenize + +Tokenize many values in one call. Each value can be tokenized against one or more named token groups. + +### Construct a bulk tokenize request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BulkTokenizeExample { + public static void main(String[] args) throws SkyflowException { + BulkTokenizeRequestRecord record1 = BulkTokenizeRequestRecord.builder() + .value("4111111111111111") + .tokenGroupNames(Arrays.asList("card_number_cg")) + .build(); + + BulkTokenizeRequestRecord record2 = BulkTokenizeRequestRecord.builder() + .value("john.doe@example.com") + .tokenGroupNames(Arrays.asList("email_cg")) + .build(); + + List records = new ArrayList<>(); + records.add(record1); + records.add(record2); + + BulkTokenizeRequest tokenizeRequest = BulkTokenizeRequest.builder() + .records(records) + .build(); + + BulkTokenizeResponse tokenizeResponse = vault.bulkTokenize(tokenizeRequest); + System.out.println(tokenizeResponse); + } +} +``` + +`BulkTokenizeRequestRecord.builder()` also accepts `token(Object)` to supply your own token for the value instead of having the vault generate one. + +### Async bulk tokenize + +```java +CompletableFuture future = vault.bulkTokenizeAsync(tokenizeRequest); +``` + +Sample response: + +```json +{ + "summary": { "totalTokens": 2, "totalTokenized": 1, "totalPartial": 0, "totalFailed": 1 }, + "records": [ + { + "index": 0, + "value": "4111111111111111", + "tokens": [ + { "tokenGroupName": "card_number_cg", "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null } + ] + }, + { + "index": 1, + "value": "john.doe@example.com", + "tokens": [ + { "tokenGroupName": "email_cg", "token": null, "httpCode": 400, "error": "Token group email_cg not found.", "requestId": "a1b2c3d4-..." } + ] + } + ] +} +``` + +Tokenize reports at **two** levels: one entry per input value in `records`, and inside each of those, one entry per requested token group in `tokens`. Because a single value can map to several token groups, the summary distinguishes fully tokenized values (`totalTokenized`), partially tokenized values where some groups succeeded and others failed (`totalPartial`), and fully failed values (`totalFailed`). The three always add up to `totalTokens`, which counts input values, not tokens produced. + +# Bulk Detokenize + +Detokenize many tokens in one call, optionally overriding the redaction applied per token group via `tokenGroupRedactions`. + +### Construct a bulk detokenize request + +```java +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.Arrays; +import java.util.List; + +public class BulkDetokenizeExample { + public static void main(String[] args) throws SkyflowException { + List tokens = new ArrayList<>(Arrays.asList( + "5479-4229-4622-1393", + "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + )); + + // redaction is a free-form string understood by the vault (e.g. "PLAIN_TEXT", + // "MASKED", "REDACTED", "DEFAULT" — the same redaction types as skyvault's RedactionType enum) + TokenGroupRedactions redaction = TokenGroupRedactions.builder() + .tokenGroupName("card_number_cg") + .redaction("MASKED") + .build(); + + BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Arrays.asList(redaction)) + .build(); + + BulkDetokenizeResponse detokenizeResponse = vault.bulkDetokenize(detokenizeRequest); + System.out.println(detokenizeResponse); + } +} +``` + +### Async bulk detokenize + +```java +CompletableFuture future = vault.bulkDetokenizeAsync(detokenizeRequest); +``` + +Sample response: + +```json +{ + "summary": { "totalTokens": 2, "totalDetokenized": 1, "totalFailed": 1 }, + "records": [ + { + "index": 0, + "requestId": null, + "value": "4111111111111111", + "tokenGroupName": "card_number_cg", + "metadata": {}, + "httpCode": 200, + "token": "5479-4229-4622-1393", + "error": null + }, + { + "index": 1, + "requestId": "a1b2c3d4-...", + "value": null, + "tokenGroupName": null, + "metadata": null, + "httpCode": 404, + "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "error": "Token Not Found" + } + ] +} +``` + +Use `detokenizeResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. + +# Bulk Delete Tokens + +Delete many tokens in one call. + +### Construct a bulk delete tokens request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BulkDeleteTokensExample { + public static void main(String[] args) throws SkyflowException { + List tokens = new ArrayList<>(Arrays.asList( + "5479-4229-4622-1393", + "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + )); + + BulkDeleteTokensRequest deleteTokensRequest = BulkDeleteTokensRequest.builder() + .tokens(tokens) + .build(); + + BulkDeleteTokensResponse deleteTokensResponse = vault.bulkDeleteTokens(deleteTokensRequest); + System.out.println(deleteTokensResponse); + } +} +``` + +### Async bulk delete tokens + +```java +CompletableFuture future = vault.bulkDeleteTokensAsync(deleteTokensRequest); +``` + +Sample response: + +```json +{ + "summary": { "totalTokens": 2, "totalDeleted": 2, "totalFailed": 0 }, + "records": [ + { "index": 0, "token": "5479-4229-4622-1393", "httpCode": 200, "error": null, "requestId": null }, + { "index": 1, "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "httpCode": 200, "error": null, "requestId": null } + ] +} +``` + +Use `deleteTokensResponse.getTokensToRetry()` to get back only the tokens worth resubmitting. + +# Custom Request Headers + +To include custom HTTP headers on an outgoing bulk request, pass a `RequestInterceptor` via that operation's options object. The headers available are defined by the `CustomHeaderKey` enum: + +| `CustomHeaderKey` | HTTP header name | +|---|---| +| `SkyflowAccountId` | `x-skyflow-account-id` | +| `SkyflowAccountName` | `x-skyflow-account-name` | +| `RequestIdHeader` | `x-request-id` | + +```java +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.vault.data.BulkInsertOptions; + +BulkInsertOptions options = BulkInsertOptions.builder() + .interceptor(context -> context.addHeader(CustomHeaderKey.RequestIdHeader, "")) + .build(); + +BulkInsertResponse insertResponse = vault.bulkInsert(insertRequest, options); +``` + +The interceptor runs **once per batch**, not once per bulk call — so a value generated inside it (a fresh request id, say) differs between the batches a single bulk call is split into. + +The same pattern applies to every bulk operation, via its corresponding options class: + +| Operation | Options class | +|---|---| +| `bulkInsert` / `bulkInsertAsync` | `BulkInsertOptions` | +| `bulkTokenize` / `bulkTokenizeAsync` | `BulkTokenizeOptions` | +| `bulkDetokenize` / `bulkDetokenizeAsync` | `BulkDetokenizeOptions` | +| `bulkDeleteTokens` / `bulkDeleteTokensAsync` | `BulkDeleteTokensOptions` | + +# Error Handling + +## Two layers of errors + +This is the mental model to hold for every bulk operation: + +| Layer | What it covers | How you see it | +|---|---|---| +| **Request-level** | The call could not be made or the whole call failed: invalid request shape, missing credentials, auth failure, payload over the 10,000-item limit. | A thrown `SkyflowException`. No results at all. | +| **Record-level** | The call succeeded, but individual records or tokens inside it did not. | A returned response. **Nothing is thrown.** Each entry in `getRecords()` reports its own `httpCode` and `error`. | + +The second layer is what distinguishes `flowvault` from an all-or-nothing API: **a bulk call that returns normally can still contain failures, and a call where every single record failed also returns normally rather than throwing.** Checking only for a thrown exception will silently miss failed records — always read the summary and the per-record results. + +## Per-record success and failure + +Every bulk response exposes `getSummary()` and `getRecords()`. The records list has one entry per submitted item, in the order you submitted it, and each entry carries: + +| Field | Present on | Meaning | +|---|---|---| +| `getIndex()` | always | Position of this item in the payload you submitted — use it to line results back up with your input. | +| `getHttpCode()` | always | Per-item status. `2xx` for success; `4xx`/`5xx` for failure. | +| `getError()` | failures only | Error message for this item. `null` means this item succeeded. | +| `getRequestId()` | failures only | The `x-request-id` of the batch this item was in — quote it in support escalations. Items from the same batch share one id. | + +The success payload sits alongside those fields on the same object: `getSkyflowId()`/`getFields()` for insert, `getValue()`/`getTokenGroupName()`/`getMetadata()` for detokenize, `getTokens()` for tokenize, `getToken()` for delete. + +Summaries per operation: + +| Response | Summary type | Fields | +|---|---|---| +| `BulkInsertResponse` | `BulkSummary` | `totalRecords`, `totalInserted`, `totalFailed` | +| `BulkTokenizeResponse` | `TokenizeSummary` | `totalTokens`, `totalTokenized`, `totalPartial`, `totalFailed` | +| `BulkDetokenizeResponse` | `DetokenizeSummary` | `totalTokens`, `totalDetokenized`, `totalFailed` | +| `BulkDeleteTokensResponse` | `DeleteTokensSummary` | `totalTokens`, `totalDeleted`, `totalFailed` | + +The idiomatic way to consume a bulk response: + +```java +BulkInsertResponse response = vault.bulkInsert(insertRequest); + +System.out.println("inserted " + response.getSummary().getTotalInserted() + + " of " + response.getSummary().getTotalRecords()); + +for (BulkInsertResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.println("row " + record.getIndex() + " -> " + record.getSkyflowId()); + } else { + System.err.println("row " + record.getIndex() + " failed [" + + record.getHttpCode() + "] " + record.getError() + + " (requestId " + record.getRequestId() + ")"); + } +} +``` + +For tokenize, the check is one level deeper, because a single value can partially succeed: + +```java +for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) { + for (TokenizeResponseToken token : record.getTokens()) { + if (token.getError() == null) { + System.out.println(record.getIndex() + "/" + token.getTokenGroupName() + + " -> " + token.getToken()); + } else { + System.err.println(record.getIndex() + "/" + token.getTokenGroupName() + + " failed [" + token.getHttpCode() + "] " + token.getError()); + } + } +} +``` + +## Catching SkyflowException + +`SkyflowException` covers the request-level layer only — client-side validation errors and whole-call API errors. It comes from `common`, so it is the same exception type `skyvault` throws. + +```java +import com.skyflow.errors.SkyflowException; + +try { + BulkInsertResponse response = vault.bulkInsert(insertRequest); + // reaching here means the CALL succeeded — individual records may still have failed +} catch (SkyflowException e) { + System.err.println("Skyflow error:"); + System.err.println(" HTTP code : " + e.getHttpCode()); + System.err.println(" Message : " + e.getMessage()); + System.err.println(" Request ID: " + e.getRequestId()); + System.err.println(" Details : " + e.getDetails()); +} catch (Exception e) { + System.err.println("Unexpected error: " + e.getMessage()); +} +``` + +For the async variants, the same exception arrives wrapped in a `CompletionException`: + +```java +vault.bulkInsertAsync(insertRequest) + .thenAccept(response -> System.out.println(response)) + .exceptionally(throwable -> { + System.err.println("bulk insert failed: " + throwable.getCause().getMessage()); + return null; + }); +``` + +## SkyflowException properties + +| Property | Method | Description | +|---|---|---| +| HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). | +| Message | `getMessage()` | Human-readable description of the error. | +| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). | +| gRPC code | `getGrpcCode()` | gRPC status code from the server. | +| Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. | +| Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. | + +**Validation errors** (table name at the wrong level, empty token list, payload over 10,000 items, and similar) are thrown before any network call: + +- `httpCode` is always `400` +- `requestId` and `grpcCode` are `null` +- `details` is an empty array + +**API errors** are returned by the Skyflow server and have all fields populated from the response body and headers. + +## Retrying the failed records + +Because failures are reported per record, a partial failure can be retried without resubmitting the whole payload. Each response exposes a retry helper that filters its records down to the ones worth resending — **server-side failures (HTTP 500–599), excluding 529**, which is a permanent capacity-limit code: + +| Response | Helper | Returns | +|---|---|---| +| `BulkInsertResponse` | `getRecordsToRetry()` | `List` — your original record objects, ready to resubmit | +| `BulkTokenizeResponse` | `getRecordsToRetry()` | `List` — values with at least one retryable token-group failure | +| `BulkDetokenizeResponse` | `getTokensToRetry()` | `List` — the tokens to resubmit | +| `BulkDeleteTokensResponse` | `getTokensToRetry()` | `List` — the tokens to resubmit | + +```java +BulkInsertResponse response = vault.bulkInsert(insertRequest); + +List retryable = response.getRecordsToRetry(); +if (!retryable.isEmpty()) { + BulkInsertResponse retryResponse = vault.bulkInsert( + BulkInsertRequest.builder() + .tableName("table1") + .records(new ArrayList<>(retryable)) + .build()); +} +``` + +Client-side (`4xx`) failures are deliberately excluded — those need a fix to the data, not a retry. This is separate from the transport-level `maxRetries` setting in [Timeouts and retries](#timeouts-and-retries), which retries whole HTTP attempts and is off by default. diff --git a/flowvault/api-report/skyflow-flowvault-java.baseline.jar b/flowvault/api-report/skyflow-flowvault-java.baseline.jar new file mode 100644 index 00000000..d98e0eb7 Binary files /dev/null and b/flowvault/api-report/skyflow-flowvault-java.baseline.jar differ diff --git a/flowvault/dependency-reduced-pom.xml b/flowvault/dependency-reduced-pom.xml new file mode 100644 index 00000000..92fc11f2 --- /dev/null +++ b/flowvault/dependency-reduced-pom.xml @@ -0,0 +1,195 @@ + + + + skyflow + com.skyflow + 1.0.0 + + 4.0.0 + skyflow-flowvault-java + ${project.groupId}:${project.artifactId} + 1.0.0 + Skyflow V3 SDK for the Java programming language + https://github.com/skyflowapi/skyflow-java/tree/main + + + skyflow + skyflow + + + + + The MIT License (MIT) + https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE + + + + + + true + src/main/resources + + + + + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + com.skyflow:common + + + + + + + + + + + jfrog + + + central + prekarilabs.jfrog.io-releases + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + snapshots + prekarilabs.jfrog.io-snapshots + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + + + maven-central + + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + central + true + true + + + + + + + central + https://central.sonatype.com/api/v1/publisher/upload + + + central-snapshots + https://central.sonatype.com/api/v1/publisher/upload + + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.18.6 + compile + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.18.6 + compile + + + io.github.cdimascio + dotenv-java + 2.2.0 + compile + + + com.google.code.gson + gson + 2.10.1 + compile + + + com.squareup.okhttp3 + okhttp + 4.12.0 + compile + + + io.jsonwebtoken + jjwt + 0.12.6 + compile + + + junit + junit + 4.13.2 + test + + + hamcrest-core + org.hamcrest + + + + + org.powermock + powermock-module-junit4 + 2.0.9 + test + + + powermock-module-junit4-common + org.powermock + + + hamcrest-core + org.hamcrest + + + + + org.powermock + powermock-api-mockito2 + 2.0.9 + test + + + powermock-api-support + org.powermock + + + mockito-core + org.mockito + + + + + + UTF-8 + 8 + false + 8 + ${project.version} + + diff --git a/flowvault/pom.xml b/flowvault/pom.xml new file mode 100644 index 00000000..5a8f014f --- /dev/null +++ b/flowvault/pom.xml @@ -0,0 +1,225 @@ + + + 4.0.0 + + com.skyflow + skyflow + 1.0.0 + ../pom.xml + + + skyflow-flowvault-java + 3.0.0-beta.13-dev.c7311820 + jar + ${project.groupId}:${project.artifactId} + Skyflow V3 SDK for the Java programming language + https://github.com/skyflowapi/skyflow-java/tree/main + + + + The MIT License (MIT) + https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE + + + + + skyflow + skyflow + + + + + 8 + 8 + UTF-8 + false + ${project.version} + + + + + + + com.skyflow + common + 1.0.0 + compile + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + com.skyflow:common + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + shade-for-japicmp + package + + shade + + + true + with-common + + + com.skyflow:common + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + 0.26.0 + + + + + ${project.basedir}/api-report/skyflow-flowvault-java.baseline.jar + + + + + ${project.build.directory}/${project.build.finalName}-with-common.jar + + + + protected + true + true + true + true + + + com.skyflow.Skyflow + com.skyflow.config + com.skyflow.enums + com.skyflow.errors + com.skyflow.serviceaccount.util + com.skyflow.vault.controller + com.skyflow.vault.data + + false + false + + + + + default-cli + verify + + cmp + + + + + + + + + + jfrog + + + central + prekarilabs.jfrog.io-releases + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + snapshots + prekarilabs.jfrog.io-snapshots + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + + + maven-central + + + central + https://central.sonatype.com/api/v1/publisher/upload + + + central-snapshots + https://central.sonatype.com/api/v1/publisher/upload + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + central + true + + false + + + + + + + + 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..86d525a7 --- /dev/null +++ b/flowvault/samples/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + org.example + skyflow-javasdk-sample + 1.0.0 + + + 8 + 8 + + + + + + com.skyflow + skyflow-flowvault-java + 3.0.0-beta.13-dev.18f8f1ba + + + + + + \ No newline at end of file diff --git a/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java new file mode 100644 index 00000000..84e8ff55 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java @@ -0,0 +1,85 @@ +package com.example.serviceaccount; + +import com.skyflow.Skyflow; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + * This example demonstrates how to configure and use the Skyflow SDK + * to detokenize sensitive data stored in a Skyflow vault. + * It includes setting up credentials, configuring the vault, and + * making a bulk detokenization request. The code also implements a retry + * mechanism to handle unauthorized access errors (HTTP 401), e.g. when + * the bearer token minted from the credentials has expired. + */ +public class BearerTokenExpiryExample { + public static void main(String[] args) { + try { + // Setting up credentials for accessing the Skyflow vault + Credentials vaultCredentials = new Credentials(); + vaultCredentials.setCredentialsString(""); + + // Configuring the Skyflow vault with necessary details + VaultConfig vaultConfig = new VaultConfig(); + vaultConfig.setVaultId(""); // Vault ID + vaultConfig.setClusterId(""); // Cluster ID + vaultConfig.setEnv(Env.PROD); // Environment (e.g., DEV, PROD) + vaultConfig.setCredentials(vaultCredentials); // Setting credentials + + // Creating a Skyflow client instance with the configured vault + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.ERROR) // Setting log level to ERROR + .addVaultConfig(vaultConfig) // Adding vault configuration + .build(); + + // Attempting to detokenize data using the Skyflow client + try { + detokenizeData(skyflowClient); + } catch (SkyflowException e) { + // Retry detokenization if the error is due to unauthorized access (HTTP 401) + if (e.getHttpCode() == 401) { + detokenizeData(skyflowClient); + } else { + // Rethrow the exception for other error codes + throw e; + } + } + } catch (SkyflowException e) { + // Handling any exceptions that occur during the process + System.out.println("An error occurred: " + e.getMessage()); + } + } + + /** + * Method to detokenize data using the Skyflow client. + * It sends a bulk detokenization request with a list of tokens and prints the response. + * + * @param skyflowClient The Skyflow client instance used for detokenization. + * @throws SkyflowException If an error occurs during the detokenization process. + */ + public static void detokenizeData(Skyflow skyflowClient) throws SkyflowException { + // Creating a list of tokens to be detokenized + List tokens = new ArrayList<>(); + tokens.add(""); // First token + tokens.add(""); // Second token + + // Building a bulk detokenization request with the token list + BulkDetokenizeRequest detokenizeRequest = BulkDetokenizeRequest.builder() + .tokens(tokens) // Adding tokens to the request + .build(); + + // Sending the detokenization request and receiving the response + BulkDetokenizeResponse detokenizeResponse = skyflowClient.vault().bulkDetokenize(detokenizeRequest); + + // Printing the detokenized response + System.out.println(detokenizeResponse); + } +} diff --git a/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/BulkDeleteTokensAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDeleteTokensAsync.java new file mode 100644 index 00000000..786d6461 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDeleteTokensAsync.java @@ -0,0 +1,93 @@ +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.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; + +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 delete tokens operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 2. Creating a list of tokens to delete + * 3. Building and executing an async bulk delete tokens request + * 4. Reading the per-token outcome and the summary from the response + * 5. Handling the delete response or errors using CompletableFuture + */ +public class BulkDeleteTokensAsync { + + 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 delete. The SDK assigns each token an index from its + // position in this list and returns it on the matching response record, so results stay + // correlated even though the batches complete out of order. + List tokens = new ArrayList<>(); + tokens.add(""); + tokens.add(""); + + // Step 5: Build the bulk delete tokens request + BulkDeleteTokensRequest deleteTokensRequest = BulkDeleteTokensRequest.builder() + .tokens(tokens) + .build(); + + // Step 6: Execute the async bulk delete tokens operation and handle response using callbacks + CompletableFuture future = + skyflowClient.vault().bulkDeleteTokensAsync(deleteTokensRequest); + future.thenAccept(response -> { + System.out.println("Async bulk delete tokens resolved with response:\t" + response); + + // Successes and failures share one list: a record succeeded when its error is null. + // requestId identifies the API call an error came from and is set on failures only. + for (BulkDeleteTokensResponseRecord record : response.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s deleted (%d)%n", + record.getIndex(), record.getToken(), record.getHttpCode()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Tokens that failed with a retryable status (5xx other than 529) can be resubmitted + if (!response.getTokensToRetry().isEmpty()) { + System.out.println("tokens to retry:\t" + response.getTokensToRetry()); + } + }).exceptionally(throwable -> { + System.err.println("Async bulk delete tokens rejected with error:\t" + throwable.getMessage()); + throw new CompletionException(throwable); + }); + } catch (SkyflowException e) { + // Step 7: 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/BulkDeleteTokensSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkDeleteTokensSync.java new file mode 100644 index 00000000..e009fc96 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkDeleteTokensSync.java @@ -0,0 +1,99 @@ +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.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; + +import java.util.ArrayList; +import java.util.List; + +/** + * This sample demonstrates how to perform a synchronous bulk delete tokens operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 2. Creating a list of tokens to delete + * 3. Building and executing a bulk delete tokens request + * 4. Reading the per-token outcome and the summary from the response + * 5. Handling the delete response or any potential errors + */ +public class BulkDeleteTokensSync { + + 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 delete. The SDK assigns each token an index from its + // position in this list and returns it on the matching response record, so results stay + // correlated even though large requests are split into batches that run concurrently. + List tokens = new ArrayList<>(); + tokens.add(""); + tokens.add(""); + + // Step 5: Build the bulk delete tokens request + BulkDeleteTokensRequest deleteTokensRequest = BulkDeleteTokensRequest.builder() + .tokens(tokens) + .build(); + + // Step 6: Execute the bulk delete tokens operation and print the response + BulkDeleteTokensResponse deleteTokensResponse = + skyflowClient.vault().bulkDeleteTokens(deleteTokensRequest); + System.out.println(deleteTokensResponse); + + // Step 7: Read the summary. totalTokens counts the tokens you submitted, and the other + // two classify each one, so together they sum to that count. + System.out.println("total tokens:\t" + deleteTokensResponse.getSummary().getTotalTokens()); + System.out.println("deleted:\t" + deleteTokensResponse.getSummary().getTotalDeleted()); + System.out.println("failed:\t\t" + deleteTokensResponse.getSummary().getTotalFailed()); + + // Step 8: Walk the per-token outcomes. Successes and failures share one list: a record + // succeeded when its error is null, and its index is the token's position in the request + // you submitted. requestId identifies the API call an error came from and is set on + // failures only. + for (BulkDeleteTokensResponseRecord record : deleteTokensResponse.getRecords()) { + if (record.getError() == null) { + System.out.printf("[%d] %s deleted (%d)%n", + record.getIndex(), record.getToken(), record.getHttpCode()); + } else { + System.out.printf("[%d] %s failed (%d): %s [requestId=%s]%n", + record.getIndex(), record.getToken(), record.getHttpCode(), + record.getError(), record.getRequestId()); + } + } + + // Step 9: Optionally retry the tokens that failed with a retryable status (5xx other + // than 529). A token that simply does not exist fails with a 4xx, so it is not included. + List tokensToRetry = deleteTokensResponse.getTokensToRetry(); + if (!tokensToRetry.isEmpty()) { + System.out.println("retrying:\t" + tokensToRetry); + BulkDeleteTokensResponse retryResponse = skyflowClient.vault().bulkDeleteTokens( + BulkDeleteTokensRequest.builder().tokens(tokensToRetry).build()); + System.out.println("retry response:\t" + retryResponse); + } + } 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/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/BulkTokenizeAsync.java b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java new file mode 100644 index 00000000..762c523b --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeAsync.java @@ -0,0 +1,112 @@ +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.BulkTokenizeRequest; +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 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 tokenize operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 2. Creating a list of records, each with a value and one or more token group names + * 3. Building and executing an async bulk tokenize request + * 4. Reading the per-token-group outcome for each value + * 5. Handling the tokenize response or errors using CompletableFuture + */ +public class BulkTokenizeAsync { + + 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: Specify the token groups to tokenize each value against + List tokenGroupNames = new ArrayList<>(); + tokenGroupNames.add(""); + tokenGroupNames.add(""); + + // Step 5: Prepare the records to tokenize. The SDK assigns each record an index from its + // position in this list and returns it on the matching response record, so results stay + // correlated even though the batches complete out of order. + List records = new ArrayList<>(); + records.add(BulkTokenizeRequestRecord.builder() + .value("") + .tokenGroupNames(tokenGroupNames) + .build()); + records.add(BulkTokenizeRequestRecord.builder() + .value("") + // Optional: supply your own token instead of having one generated (BYOT). + // A BYOT record must name exactly one token group. + // .token("") + .tokenGroupNames(tokenGroupNames) + .build()); + + // Step 6: Build the bulk tokenize request + BulkTokenizeRequest tokenizeRequest = BulkTokenizeRequest.builder() + .records(records) + .build(); + + // Step 7: Execute the async bulk tokenize operation and handle response using callbacks + CompletableFuture future = + skyflowClient.vault().bulkTokenizeAsync(tokenizeRequest); + future.thenAccept(response -> { + System.out.println("Async bulk tokenize resolved with response:\t" + response); + + // Each value reports one entry per token group, so a value can partially succeed. + // requestId identifies the API call an error came from and is set on failures only. + for (BulkTokenizeResponseRecord record : response.getRecords()) { + for (TokenizeResponseToken token : record.getTokens()) { + if (token.getError() == null) { + System.out.printf("[%d] group '%s' -> %s%n", + record.getIndex(), token.getTokenGroupName(), token.getToken()); + } else { + System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", + record.getIndex(), token.getTokenGroupName(), + token.getHttpCode(), token.getError(), token.getRequestId()); + } + } + } + + // Records that failed with a retryable status (5xx other than 529) come back + // unchanged and can be resubmitted as-is. + if (!response.getRecordsToRetry().isEmpty()) { + System.out.println("records to retry:\t" + response.getRecordsToRetry().size()); + } + }).exceptionally(throwable -> { + System.err.println("Async bulk tokenize 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/BulkTokenizeSync.java b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java new file mode 100644 index 00000000..9690f423 --- /dev/null +++ b/flowvault/samples/src/main/java/com/example/vault/BulkTokenizeSync.java @@ -0,0 +1,115 @@ +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.BulkTokenizeRequest; +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 java.util.ArrayList; +import java.util.List; + +/** + * This sample demonstrates how to perform a synchronous bulk tokenize operation using the Skyflow Java SDK. + * The process involves: + * 1. Setting up credentials and vault configuration + * 2. Creating a list of records, each with a value and one or more token group names + * 3. Building and executing a bulk tokenize request + * 4. Reading the per-token-group outcome for each value + * 5. Handling the tokenize response or any potential errors + */ +public class BulkTokenizeSync { + + 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: Specify the token groups to tokenize each value against + List tokenGroupNames = new ArrayList<>(); + tokenGroupNames.add(""); + tokenGroupNames.add(""); + + // Step 5: Prepare the records to tokenize. The SDK assigns each record an index from its + // position in this list and returns it on the matching response record, so results stay + // correlated even though large requests are split into batches that run concurrently. + List records = new ArrayList<>(); + records.add(BulkTokenizeRequestRecord.builder() + .value("") + .tokenGroupNames(tokenGroupNames) + .build()); + records.add(BulkTokenizeRequestRecord.builder() + .value("") + // Optional: supply your own token instead of having one generated (BYOT). + // A BYOT record must name exactly one token group. + // .token("") + .tokenGroupNames(tokenGroupNames) + .build()); + + // Step 6: Build the bulk tokenize request + BulkTokenizeRequest tokenizeRequest = BulkTokenizeRequest.builder() + .records(records) + .build(); + + // Step 7: Execute the bulk tokenize operation and print the response + BulkTokenizeResponse tokenizeResponse = skyflowClient.vault().bulkTokenize(tokenizeRequest); + System.out.println(tokenizeResponse); + + // Step 8: Read the summary. totalTokens counts the values you submitted; the other three + // classify each value by how its token groups fared, so together they sum to that count. + System.out.println("total values:\t" + tokenizeResponse.getSummary().getTotalTokens()); + System.out.println("tokenized:\t" + tokenizeResponse.getSummary().getTotalTokenized()); + System.out.println("partial:\t" + tokenizeResponse.getSummary().getTotalPartial()); + System.out.println("failed:\t\t" + tokenizeResponse.getSummary().getTotalFailed()); + + // Step 9: Walk the results. Each value reports one entry per token group, so a value can + // partially succeed: some groups return a token while others return an error. requestId + // identifies the API call an error came from and is set on failures only. + for (BulkTokenizeResponseRecord record : tokenizeResponse.getRecords()) { + for (TokenizeResponseToken token : record.getTokens()) { + if (token.getError() == null) { + System.out.printf("[%d] group '%s' -> %s%n", + record.getIndex(), token.getTokenGroupName(), token.getToken()); + } else { + System.out.printf("[%d] group '%s' failed (%d): %s [requestId=%s]%n", + record.getIndex(), token.getTokenGroupName(), + token.getHttpCode(), token.getError(), token.getRequestId()); + } + } + } + + // Step 10: Optionally retry the records that failed with a retryable status (5xx other + // than 529). Your original records come back unchanged and can be resubmitted as-is. + List recordsToRetry = tokenizeResponse.getRecordsToRetry(); + if (!recordsToRetry.isEmpty()) { + BulkTokenizeResponse retryResponse = skyflowClient.vault().bulkTokenize( + BulkTokenizeRequest.builder().records(recordsToRetry).build()); + System.out.println("retry response:\t" + retryResponse); + } + } catch (SkyflowException e) { + // Step 11: Handle any errors that occur during the process + System.err.println("Error in Skyflow operations: " + e.getMessage()); + } + } +} diff --git a/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java b/flowvault/samples/src/main/java/com/example/vault/CustomHeaderExample.java new file mode 100644 index 00000000..4dc4d67c --- /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.BulkInsertOptions; +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 SKYFLOW_ACCOUNT_ID} ({@code x-skyflow-account-id}), + * {@code SKYFLOW_ACCOUNT_NAME} ({@code x-skyflow-account-name}) and {@code REQUEST_ID_HEADER} + * ({@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 + BulkInsertOptions options = BulkInsertOptions.builder() + .interceptor(ctx -> { + ctx.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, 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 new file mode 100644 index 00000000..5c2e682c --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/Skyflow.java @@ -0,0 +1,305 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.LogLevel; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.Constants; +import com.skyflow.utils.SdkVersion; +import com.skyflow.utils.Utils; +import com.skyflow.utils.logger.LogUtil; +import com.skyflow.utils.validations.Validations; +import com.skyflow.vault.controller.VaultController; + +import java.util.LinkedHashMap; + +public final class Skyflow extends BaseSkyflow { + + private final SkyflowClientBuilder builder; + + private Skyflow(SkyflowClientBuilder builder) { + super(builder); + this.builder = builder; + } + + @Override + protected Skyflow self() { + return this; + } + + public static SkyflowClientBuilder builder() { + SdkVersion.setSdkPrefix(Constants.SDK_PREFIX); + return new SkyflowClientBuilder(); + } + + public VaultConfig getVaultConfig() { + Object[] array = this.builder.vaultConfigMap.values().toArray(); + return (VaultConfig) array[0]; + } + + /** + * Updates a vault's configuration on an already-built client. + *

+ * BaseSkyflow.updateVaultConfig goes straight to the template, bypassing the builder's own + * override, so the flowvault-specific fields have to be carried across here too — otherwise + * a vaultUrl or HTTP setting supplied through this entry point would be silently dropped + * while the same call on the builder honoured it. + */ + @Override + public Skyflow updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + super.updateVaultConfig(vaultConfig); + this.builder.carryVaultOverrides(vaultConfig); + return this; + } + + public VaultController vault() throws SkyflowException { + return resolveOrThrow(this.builder.vaultClientsMap, null, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); + } + + public VaultController vault(String vaultId) throws SkyflowException { + return resolveOrThrow(this.builder.vaultClientsMap, vaultId, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); + } + + + public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder { + private final LinkedHashMap vaultClientsMap = new LinkedHashMap<>(); + // Client-wide HTTP config. Resolution per vault, most specific first: + // VaultConfig value -> the value set here -> SDK default (60s call timeout, 0 retries). + // null here means "not set", so the SDK default applies to vaults that don't override it. + // Only null means inherit: an explicit 0 is a real value and wins over the level below. + private Integer timeout; + private Integer connectTimeout; + private Integer readTimeout; + private Integer writeTimeout; + private Integer maxRetries; + private Long initialRetryDelayMillis; + private Long maxRetryDelayMillis; + + @Override + protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + Validations.validateVaultConfiguration(vaultConfig); + } + + @Override + 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.initialRetryDelayMillis, + this.maxRetryDelayMillis); + this.vaultClientsMap.put(vaultConfig.getVaultId(), controller); + LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); + } + + @Override + protected void onVaultConfigUpdated(VaultConfig updatedConfig) throws SkyflowException { + // Update the existing controller in place — replacing it would leave any VaultController + // reference the caller already holds pointing at the previous config. + VaultController updated = this.vaultClientsMap.get(updatedConfig.getVaultId()); + if (updated == null) { + updated = new VaultController(updatedConfig, this.skyflowCredentials); + this.vaultClientsMap.put(updatedConfig.getVaultId(), updated); + } else { + updated.setVaultConfig(updatedConfig); + } + updated.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis, + this.maxRetryDelayMillis); + } + + @Override + protected void onVaultConfigRemoved(String vaultId) throws SkyflowException { + this.vaultClientsMap.remove(vaultId); + } + + @Override + protected boolean hasVaultClient(String vaultId) { + return this.vaultClientsMap.containsKey(vaultId); + } + + @Override + protected void onCredentialsUpdated(Credentials credentials) throws SkyflowException { + for (VaultController vault : this.vaultClientsMap.values()) { + vault.setCommonCredentials(credentials); + } + } + + @Override + public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + super.addVaultConfig(vaultConfig); + return this; + } + + @Override + public SkyflowClientBuilder updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + super.updateVaultConfig(vaultConfig); + carryVaultOverrides(vaultConfig); + return this; + } + + /** + * BaseSkyflow.mergeVaultConfig() only carries env, clusterId and credentials across, so the + * flowvault-specific fields on an incoming update — vaultUrl and the HTTP settings — would + * be dropped silently. Apply them to the merged config the new controller is holding. A null + * on the incoming config means "leave as is", matching how the base class merges every + * other field. + */ + private void carryVaultOverrides(VaultConfig incoming) throws SkyflowException { + VaultConfig merged = this.vaultConfigMap.get(incoming.getVaultId()); + if (merged == null || merged == incoming) { + return; + } + if (incoming.getTimeout() != null) { + merged.setTimeout(incoming.getTimeout()); + } + if (incoming.getConnectTimeout() != null) { + merged.setConnectTimeout(incoming.getConnectTimeout()); + } + if (incoming.getReadTimeout() != null) { + merged.setReadTimeout(incoming.getReadTimeout()); + } + if (incoming.getWriteTimeout() != null) { + merged.setWriteTimeout(incoming.getWriteTimeout()); + } + 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) { + merged.setVaultUrl(incoming.getVaultUrl()); + VaultController controller = this.vaultClientsMap.get(incoming.getVaultId()); + if (controller != null) { + controller.refreshVaultUrl(); + } + } + } + + @Override + public SkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { + super.removeVaultConfig(vaultId); + return this; + } + + @Override + public SkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throws SkyflowException { + super.addSkyflowCredentials(credentials); + return this; + } + + @Override + public SkyflowClientBuilder setLogLevel(LogLevel logLevel) { + super.setLogLevel(logLevel); + return this; + } + + /** + * Overall call timeout in seconds, including retries. Default 60. + *

+ * Precedence: a vault that sets {@link VaultConfig#setTimeout(Integer)} wins; this + * value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder timeout(int timeout) { + this.timeout = timeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt connection-establishment timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setConnectTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder connectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt response-read timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setReadTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder readTimeout(int readTimeout) { + this.readTimeout = readTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Per-attempt request-write timeout in seconds. Unset => HTTP client default (10s). + *

+ * Precedence: a vault that sets {@link VaultConfig#setWriteTimeout(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder writeTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Retry attempts after the first failure. Default 0 — retries are opt-in so non-idempotent + * bulk writes are not replayed automatically. + *

+ * Precedence: a vault that sets {@link VaultConfig#setMaxRetries(Integer)} wins; + * this value applies only to vaults that leave it unset. + */ + public SkyflowClientBuilder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + propagateHttpConfig(); + 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.initialRetryDelayMillis, + this.maxRetryDelayMillis); + } + } + + public Skyflow build() { + return new Skyflow(this); + } + } + +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/VaultClient.java b/flowvault/src/main/java/com/skyflow/VaultClient.java new file mode 100644 index 00000000..d45d50fc --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/VaultClient.java @@ -0,0 +1,188 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.ApiClientBuilder; +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; + +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import okhttp3.Request; + +public class VaultClient extends BaseVaultClient { + private final ApiClientBuilder apiClientBuilder; + private ApiClient apiClient; + // Client-wide (Skyflow builder) HTTP config; null => fall back to the SDK defaults below. + private Integer commonTimeout; + private Integer commonConnectTimeout; + 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); + this.apiClientBuilder = new ApiClientBuilder(); + this.apiClient = null; + updateVaultUrl(); + } + + /** + * Applies the client-wide HTTP settings from the Skyflow builder. Discards the cached HTTP + * 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, + 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; + } + + /** Resolve a setting: vault-level override, else client-wide default, else the SDK default. */ + private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defaultValue) { + if (vaultLevel != null) { + return vaultLevel; + } + 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. + */ + private static Integer resolveNullableInt(Integer vaultLevel, Integer clientLevel) { + return vaultLevel != null ? vaultLevel : clientLevel; + } + + protected FlowserviceClient getRecordsApi() { + return this.apiClient.flowservice(); + } + + protected RecordsClient getQueryApi() { + return this.apiClient.records(); + } + + protected void setCommonCredentials(Credentials commonCredentials) throws SkyflowException { + this.commonCredentials = commonCredentials; + super.prioritiseCredentials(this.vaultConfig.getCredentials()); + } + + protected synchronized void setBearerToken() throws SkyflowException { + super.setBearerToken(this.vaultConfig.getCredentials()); + if (apiClient == null) { + updateExecutorInHTTP(); + this.apiClient = this.apiClientBuilder.build(); + } + } + + /** + * Adopts an updated config in place, so a VaultController reference the caller already holds + * keeps working instead of silently serving the previous config. Discards the cached HTTP and + * API clients; the bearer token is re-resolved by setBearerToken, which drops it when the + * effective credentials changed. + */ + protected void setVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + this.vaultConfig = vaultConfig; + this.sharedHttpClient = null; + this.apiClient = null; + updateVaultUrl(); + } + + /** + * Re-resolves the vault URL from the current config. The constructor resolves it once, so a + * vaultUrl supplied later through updateVaultConfig would otherwise never take effect. + */ + protected void refreshVaultUrl() throws SkyflowException { + updateVaultUrl(); + } + + private void updateVaultUrl() throws SkyflowException { + // Fetch vaultUrl from ENV + String vaultUrl = Utils.getEnvVaultUrl(); + + // If vaultUrl from ENV is null or empty, fetch vaultUrl from vault config + if (vaultUrl == null || vaultUrl.isEmpty()) { + vaultUrl = this.vaultConfig.getVaultUrl(); + } + + // If vaultUrl from vault config is also null or empty, construct vaultUrl from clusterId passed in vault config + if (vaultUrl == null || vaultUrl.isEmpty()) { + vaultUrl = Utils.getVaultUrl(this.vaultConfig.getClusterId(), this.vaultConfig.getEnv()); + } + this.apiClientBuilder.url(vaultUrl); + if (!vaultUrl.equals(this.currentVaultURL)) { + this.currentVaultURL = vaultUrl; + this.apiClient = null; + } + } + + 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); + Integer writeTimeout = resolveNullableInt(vaultConfig.getWriteTimeout(), commonWriteTimeout); + + OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) + // Overall ceiling; bounds the whole call including retries. + .callTimeout(timeoutSeconds, TimeUnit.SECONDS) + // OUTER: retries. Must wrap the auth interceptor so each attempt re-reads the + // (possibly refreshed) bearer token rather than replaying a stale one. + .addInterceptor(new SkyflowRetryInterceptor(maxRetries, initialRetryDelayMillis, maxRetryDelayMillis)) + .addInterceptor(chain -> { // INNER: auth + Request requestWithAuth = chain.request().newBuilder() + .header("Authorization", "Bearer " + this.token) + .build(); + return chain.proceed(requestWithAuth); + }); + if (connectTimeout != null) { + httpBuilder.connectTimeout(connectTimeout, TimeUnit.SECONDS); + } + if (readTimeout != null) { + httpBuilder.readTimeout(readTimeout, TimeUnit.SECONDS); + } + if (writeTimeout != null) { + httpBuilder.writeTimeout(writeTimeout, TimeUnit.SECONDS); + } + sharedHttpClient = httpBuilder.build(); + apiClientBuilder.httpClient(sharedHttpClient); + } + } + +} diff --git a/flowvault/src/main/java/com/skyflow/config/VaultConfig.java b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java new file mode 100644 index 00000000..3369abc7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/config/VaultConfig.java @@ -0,0 +1,152 @@ +package com.skyflow.config; + +/** + * Per-vault configuration. + *

+ * The HTTP timeout and retry settings below are vault-level overrides. Each one resolves + * most-specific-first: the value set here, else the client-wide value set on + * {@code Skyflow.builder()}, else the SDK default. So when the same setting is supplied at both + * levels, the value on this VaultConfig takes precedence and the client-wide value is + * ignored for this vault. + *

+ * Only {@code null} means "inherit" — an explicit {@code 0} is a real value and wins over the + * client-wide setting. + */ +public class VaultConfig extends BaseVaultConfig { + + private String vaultUrl; + // HTTP timeout & retry config (vault-level overrides). null => inherit client-wide default, then SDK default. + private Integer timeout; // overall call timeout, in seconds + private Integer connectTimeout; // per-attempt connection-establishment timeout, in seconds + 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(); + this.vaultUrl = null; + this.timeout = null; + this.connectTimeout = null; + this.readTimeout = null; + this.writeTimeout = null; + this.maxRetries = null; + this.initialRetryDelayMillis = null; + this.maxRetryDelayMillis = null; + } + + public String getVaultUrl() { + return vaultUrl; + } + + public void setVaultUrl(String vaultUrl) { + this.vaultUrl = vaultUrl; + } + + public Integer getTimeout() { + return timeout; + } + + /** + * Overall call timeout in seconds for this vault, including retries. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().timeout(...)}. Leave unset + * (null) to inherit that value, or the SDK default of 60s if it is also unset. + */ + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + /** + * Per-attempt connection-establishment timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().connectTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + /** + * Per-attempt response-read timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().readTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + + public Integer getWriteTimeout() { + return writeTimeout; + } + + /** + * Per-attempt request-write timeout in seconds for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().writeTimeout(...)}. Leave + * unset (null) to inherit that value; if neither is set, the underlying HTTP client default + * (10s) applies. Note the overall {@code timeout} still bounds the whole call, including retries. + */ + public void setWriteTimeout(Integer writeTimeout) { + this.writeTimeout = writeTimeout; + } + + public Integer getMaxRetries() { + return maxRetries; + } + + /** + * Retry attempts after the first failure for this vault. + *

+ * Takes precedence over the client-wide {@code Skyflow.builder().maxRetries(...)}. Leave unset + * (null) to inherit that value, or the SDK default of 0 if it is also unset — retries are + * opt-in, so non-idempotent bulk writes are not replayed silently. + */ + 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/enums/CustomHeaderKey.java b/flowvault/src/main/java/com/skyflow/enums/CustomHeaderKey.java new file mode 100644 index 00000000..9a3077f2 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/enums/CustomHeaderKey.java @@ -0,0 +1,18 @@ +package com.skyflow.enums; + +public enum CustomHeaderKey { + SKYFLOW_ACCOUNT_ID("x-skyflow-account-id"), + SKYFLOW_ACCOUNT_NAME("x-skyflow-account-name"), + REQUEST_ID_HEADER("x-request-id"); + + private final String value; + + CustomHeaderKey(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } +} diff --git a/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java b/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java new file mode 100644 index 00000000..475f8ad2 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/enums/InterfaceName.java @@ -0,0 +1,21 @@ +package com.skyflow.enums; + +public enum InterfaceName { + INSERT("insert"), + DETOKENIZE("detokenize"), + DELETE("delete tokens"), + TOKENIZE("tokenize"), + QUERY("query"), + GET("get"); + + + private final String interfaceName; + + InterfaceName(String interfaceName) { + this.interfaceName = interfaceName; + } + + public String getName() { + return interfaceName; + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/ApiClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/ApiClient.java new file mode 100644 index 00000000..02b972dd --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/ApiClient.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.Suppliers; +import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; +import com.skyflow.generated.rest.resources.records.RecordsClient; + +import java.util.function.Supplier; + +public class ApiClient { + protected final ClientOptions clientOptions; + + protected final Supplier recordsClient; + + protected final Supplier flowserviceClient; + + public ApiClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.recordsClient = Suppliers.memoize(() -> new RecordsClient(clientOptions)); + this.flowserviceClient = Suppliers.memoize(() -> new FlowserviceClient(clientOptions)); + } + + public RecordsClient records() { + return this.recordsClient.get(); + } + + public FlowserviceClient flowservice() { + return this.flowserviceClient.get(); + } + + public static ApiClientBuilder builder() { + return new ApiClientBuilder(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java b/flowvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java new file mode 100644 index 00000000..fa3d6e9d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java @@ -0,0 +1,48 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.Environment; +import okhttp3.OkHttpClient; + +public final class ApiClientBuilder { + private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + + private Environment environment; + + public ApiClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public ApiClientBuilder timeout(int timeout) { + this.clientOptionsBuilder.timeout(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public ApiClientBuilder maxRetries(int maxRetries) { + this.clientOptionsBuilder.maxRetries(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public ApiClientBuilder httpClient(OkHttpClient httpClient) { + this.clientOptionsBuilder.httpClient(httpClient); + return this; + } + + public ApiClient build() { + clientOptionsBuilder.environment(this.environment); + return new ApiClient(clientOptionsBuilder.build()); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java new file mode 100644 index 00000000..840c0bad --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.Suppliers; +import com.skyflow.generated.rest.resources.flowservice.AsyncFlowserviceClient; +import com.skyflow.generated.rest.resources.records.AsyncRecordsClient; + +import java.util.function.Supplier; + +public class AsyncApiClient { + protected final ClientOptions clientOptions; + + protected final Supplier recordsClient; + + protected final Supplier flowserviceClient; + + public AsyncApiClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.recordsClient = Suppliers.memoize(() -> new AsyncRecordsClient(clientOptions)); + this.flowserviceClient = Suppliers.memoize(() -> new AsyncFlowserviceClient(clientOptions)); + } + + public AsyncRecordsClient records() { + return this.recordsClient.get(); + } + + public AsyncFlowserviceClient flowservice() { + return this.flowserviceClient.get(); + } + + public static AsyncApiClientBuilder builder() { + return new AsyncApiClientBuilder(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java b/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java new file mode 100644 index 00000000..10c08d51 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java @@ -0,0 +1,48 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.Environment; +import okhttp3.OkHttpClient; + +public final class AsyncApiClientBuilder { + private ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + + private Environment environment; + + public AsyncApiClientBuilder url(String url) { + this.environment = Environment.custom(url); + return this; + } + + /** + * Sets the timeout (in seconds) for the client. Defaults to 60 seconds. + */ + public AsyncApiClientBuilder timeout(int timeout) { + this.clientOptionsBuilder.timeout(timeout); + return this; + } + + /** + * Sets the maximum number of retries for the client. Defaults to 2 retries. + */ + public AsyncApiClientBuilder maxRetries(int maxRetries) { + this.clientOptionsBuilder.maxRetries(maxRetries); + return this; + } + + /** + * Sets the underlying OkHttp client + */ + public AsyncApiClientBuilder httpClient(OkHttpClient httpClient) { + this.clientOptionsBuilder.httpClient(httpClient); + return this; + } + + public AsyncApiClient build() { + clientOptionsBuilder.environment(this.environment); + return new AsyncApiClient(clientOptionsBuilder.build()); + } +} diff --git a/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java index 4fab1d41..be5247eb 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java @@ -3,11 +3,12 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.Response; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import okhttp3.Response; /** * This exception type will be thrown for any non-2XX API responses. diff --git a/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/ApiClientException.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java diff --git a/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java index 9c81f1f5..c743352c 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java @@ -3,11 +3,12 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.Response; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import okhttp3.Response; public final class ApiClientHttpResponse { diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java new file mode 100644 index 00000000..2a5f0710 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java @@ -0,0 +1,171 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.OkHttpClient; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class ClientOptions { + private final Environment environment; + + private final Map headers; + + private final Map> headerSuppliers; + + private final OkHttpClient httpClient; + + private final int timeout; + + private ClientOptions( + Environment environment, + Map headers, + Map> headerSuppliers, + OkHttpClient httpClient, + int timeout) { + this.environment = environment; + this.headers = new HashMap<>(); + this.headers.putAll(headers); + this.headers.putAll(new HashMap() { + { + put("X-Fern-Language", "JAVA"); + put("X-Fern-SDK-Name", "com.skyflow.fern:api-sdk"); + put("X-Fern-SDK-Version", "0.0.98"); + } + }); + this.headerSuppliers = headerSuppliers; + this.httpClient = httpClient; + this.timeout = timeout; + } + + public Environment environment() { + return this.environment; + } + + public Map headers(RequestOptions requestOptions) { + Map values = new HashMap<>(this.headers); + headerSuppliers.forEach((key, supplier) -> { + values.put(key, supplier.get()); + }); + if (requestOptions != null) { + values.putAll(requestOptions.getHeaders()); + } + return values; + } + + public int timeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.timeout; + } + return requestOptions.getTimeout().orElse(this.timeout); + } + + public OkHttpClient httpClient() { + return this.httpClient; + } + + public OkHttpClient httpClientWithTimeout(RequestOptions requestOptions) { + if (requestOptions == null) { + return this.httpClient; + } + return this.httpClient + .newBuilder() + .callTimeout(requestOptions.getTimeout().get(), requestOptions.getTimeoutTimeUnit()) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Environment environment; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + private int maxRetries = 2; + + private Optional timeout = Optional.empty(); + + private OkHttpClient httpClient = null; + + public Builder environment(Environment environment) { + this.environment = environment; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(int timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + /** + * Override the timeout in seconds. Defaults to 60 seconds. + */ + public Builder timeout(Optional timeout) { + this.timeout = timeout; + return this; + } + + /** + * Override the maximum number of retries. Defaults to 2 retries. + */ + public Builder maxRetries(int maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + public Builder httpClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + public ClientOptions build() { + OkHttpClient.Builder httpClientBuilder = + this.httpClient != null ? this.httpClient.newBuilder() : new OkHttpClient.Builder(); + + if (this.httpClient != null) { + timeout.ifPresent(timeout -> httpClientBuilder + .callTimeout(timeout, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS)); + } else { + httpClientBuilder + .callTimeout(this.timeout.orElse(60), TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .addInterceptor(new RetryInterceptor(this.maxRetries)); + } + + this.httpClient = httpClientBuilder.build(); + this.timeout = Optional.of(httpClient.callTimeoutMillis() / 1000); + + return new ClientOptions(environment, headers, headerSuppliers, httpClient, this.timeout.get()); + } + } +} diff --git a/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java index 6be10979..a0a6d7c4 100644 --- a/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; + import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/core/Environment.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/Environment.java new file mode 100644 index 00000000..2fc27c36 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/Environment.java @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +public final class Environment { + private final String url; + + private Environment(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + + public static Environment custom(String url) { + return new Environment(url); + } +} diff --git a/src/main/java/com/skyflow/generated/rest/core/FileStream.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/FileStream.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java index 6b459431..2131b0a4 100644 --- a/src/main/java/com/skyflow/generated/rest/core/FileStream.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java @@ -3,12 +3,13 @@ */ package com.skyflow.generated.rest.core; -import java.io.InputStream; -import java.util.Objects; import okhttp3.MediaType; import okhttp3.RequestBody; import org.jetbrains.annotations.Nullable; +import java.io.InputStream; +import java.util.Objects; + /** * Represents a file stream with associated metadata for file uploads. */ diff --git a/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java index 545f6088..55c3c971 100644 --- a/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java @@ -3,9 +3,6 @@ */ package com.skyflow.generated.rest.core; -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; import okhttp3.MediaType; import okhttp3.RequestBody; import okhttp3.internal.Util; @@ -14,6 +11,10 @@ import okio.Source; import org.jetbrains.annotations.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + /** * A custom implementation of OkHttp's RequestBody that wraps an InputStream. * This class allows streaming of data from an InputStream directly to an HTTP request body, diff --git a/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/MediaTypes.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java diff --git a/src/main/java/com/skyflow/generated/rest/core/Nullable.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/Nullable.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java diff --git a/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java diff --git a/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java index 3b7894e0..acec32b4 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + import java.io.IOException; public final class ObjectMappers { diff --git a/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java similarity index 97% rename from src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java index e9e18fb9..c0687736 100644 --- a/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java @@ -7,14 +7,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; import okhttp3.HttpUrl; import okhttp3.MultipartBody; +import java.util.*; + public class QueryStringMapper { private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java new file mode 100644 index 00000000..b8a8d14b --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/RequestOptions.java @@ -0,0 +1,87 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public final class RequestOptions { + private final Optional timeout; + + private final TimeUnit timeoutTimeUnit; + + private final Map headers; + + private final Map> headerSuppliers; + + private RequestOptions( + Optional timeout, + TimeUnit timeoutTimeUnit, + Map headers, + Map> headerSuppliers) { + this.timeout = timeout; + this.timeoutTimeUnit = timeoutTimeUnit; + this.headers = headers; + this.headerSuppliers = headerSuppliers; + } + + public Optional getTimeout() { + return timeout; + } + + public TimeUnit getTimeoutTimeUnit() { + return timeoutTimeUnit; + } + + public Map getHeaders() { + Map headers = new HashMap<>(); + headers.putAll(this.headers); + this.headerSuppliers.forEach((key, supplier) -> { + headers.put(key, supplier.get()); + }); + return headers; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Optional timeout = Optional.empty(); + + private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS; + + private final Map headers = new HashMap<>(); + + private final Map> headerSuppliers = new HashMap<>(); + + public Builder timeout(Integer timeout) { + this.timeout = Optional.of(timeout); + return this; + } + + public Builder timeout(Integer timeout, TimeUnit timeoutTimeUnit) { + this.timeout = Optional.of(timeout); + this.timeoutTimeUnit = timeoutTimeUnit; + return this; + } + + public Builder addHeader(String key, String value) { + this.headers.put(key, value); + return this; + } + + public Builder addHeader(String key, Supplier value) { + this.headerSuppliers.put(key, value); + return this; + } + + public RequestOptions build() { + return new RequestOptions(timeout, timeoutTimeUnit, headers, headerSuppliers); + } + } +} diff --git a/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java index d8df7715..1bb0b5dc 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java @@ -3,9 +3,10 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.Response; + import java.io.FilterInputStream; import java.io.IOException; -import okhttp3.Response; /** * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the diff --git a/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java index ed894407..e6c1a525 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java @@ -3,9 +3,10 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.Response; + import java.io.FilterReader; import java.io.IOException; -import okhttp3.Response; /** * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the diff --git a/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java index eda7d265..7a28c3c9 100644 --- a/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java +++ b/flowvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java @@ -3,12 +3,13 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.Interceptor; +import okhttp3.Response; + import java.io.IOException; import java.time.Duration; import java.util.Optional; import java.util.Random; -import okhttp3.Interceptor; -import okhttp3.Response; public class RetryInterceptor implements Interceptor { diff --git a/src/main/java/com/skyflow/generated/rest/core/Stream.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/Stream.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/Stream.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/Stream.java diff --git a/src/main/java/com/skyflow/generated/rest/core/Suppliers.java b/flowvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java similarity index 100% rename from src/main/java/com/skyflow/generated/rest/core/Suppliers.java rename to flowvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java new file mode 100644 index 00000000..1fd59fe9 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncFlowserviceClient.java @@ -0,0 +1,129 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.resources.flowservice.requests.*; +import com.skyflow.generated.rest.types.*; + +import java.util.concurrent.CompletableFuture; + +public class AsyncFlowserviceClient { + protected final ClientOptions clientOptions; + + private final AsyncRawFlowserviceClient rawClient; + + public AsyncFlowserviceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawFlowserviceClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawFlowserviceClient withRawResponse() { + return this.rawClient; + } + + public CompletableFuture delete() { + return this.rawClient.delete().thenApply(response -> response.body()); + } + + public CompletableFuture delete(V1DeleteRequest request) { + return this.rawClient.delete(request).thenApply(response -> response.body()); + } + + public CompletableFuture delete(V1DeleteRequest request, RequestOptions requestOptions) { + return this.rawClient.delete(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture get() { + return this.rawClient.get().thenApply(response -> response.body()); + } + + public CompletableFuture get(V1GetRequest request) { + return this.rawClient.get(request).thenApply(response -> response.body()); + } + + public CompletableFuture get(V1GetRequest request, RequestOptions requestOptions) { + return this.rawClient.get(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture insert() { + return this.rawClient.insert().thenApply(response -> response.body()); + } + + public CompletableFuture insert(V1InsertRequest request) { + return this.rawClient.insert(request).thenApply(response -> response.body()); + } + + public CompletableFuture insert(V1InsertRequest request, RequestOptions requestOptions) { + return this.rawClient.insert(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture update() { + return this.rawClient.update().thenApply(response -> response.body()); + } + + public CompletableFuture update(V1UpdateRequest request) { + return this.rawClient.update(request).thenApply(response -> response.body()); + } + + public CompletableFuture update(V1UpdateRequest request, RequestOptions requestOptions) { + return this.rawClient.update(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture deletetoken() { + return this.rawClient.deletetoken().thenApply(response -> response.body()); + } + + public CompletableFuture deletetoken(V1FlowDeleteTokenRequest request) { + return this.rawClient.deletetoken(request).thenApply(response -> response.body()); + } + + public CompletableFuture deletetoken( + V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { + return this.rawClient.deletetoken(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture detokenize() { + return this.rawClient.detokenize().thenApply(response -> response.body()); + } + + public CompletableFuture detokenize(V1FlowDetokenizeRequest request) { + return this.rawClient.detokenize(request).thenApply(response -> response.body()); + } + + public CompletableFuture detokenize( + V1FlowDetokenizeRequest request, RequestOptions requestOptions) { + return this.rawClient.detokenize(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture tokenize() { + return this.rawClient.tokenize().thenApply(response -> response.body()); + } + + public CompletableFuture tokenize(V1FlowTokenizeRequest request) { + return this.rawClient.tokenize(request).thenApply(response -> response.body()); + } + + public CompletableFuture tokenize( + V1FlowTokenizeRequest request, RequestOptions requestOptions) { + return this.rawClient.tokenize(request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture flowvaultmetrics() { + return this.rawClient.flowvaultmetrics().thenApply(response -> response.body()); + } + + public CompletableFuture flowvaultmetrics(V1FlowVaultMetricsRequest request) { + return this.rawClient.flowvaultmetrics(request).thenApply(response -> response.body()); + } + + public CompletableFuture flowvaultmetrics( + V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { + return this.rawClient.flowvaultmetrics(request, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java new file mode 100644 index 00000000..17f85d0c --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/AsyncRawFlowserviceClient.java @@ -0,0 +1,533 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.skyflow.generated.rest.core.*; +import com.skyflow.generated.rest.resources.flowservice.requests.*; +import com.skyflow.generated.rest.types.*; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public class AsyncRawFlowserviceClient { + protected final ClientOptions clientOptions; + + public AsyncRawFlowserviceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public CompletableFuture> delete() { + return delete(V1DeleteRequest.builder().build()); + } + + public CompletableFuture> delete(V1DeleteRequest request) { + return delete(request, null); + } + + public CompletableFuture> delete( + V1DeleteRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/delete") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1DeleteResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> get() { + return get(V1GetRequest.builder().build()); + } + + public CompletableFuture> get(V1GetRequest request) { + return get(request, null); + } + + public CompletableFuture> get( + V1GetRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/get") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1GetResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> insert() { + return insert(V1InsertRequest.builder().build()); + } + + public CompletableFuture> insert(V1InsertRequest request) { + return insert(request, null); + } + + public CompletableFuture> insert( + V1InsertRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/insert") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1InsertResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> update() { + return update(V1UpdateRequest.builder().build()); + } + + public CompletableFuture> update(V1UpdateRequest request) { + return update(request, null); + } + + public CompletableFuture> update( + V1UpdateRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/update") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1UpdateResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> deletetoken() { + return deletetoken(V1FlowDeleteTokenRequest.builder().build()); + } + + public CompletableFuture> deletetoken( + V1FlowDeleteTokenRequest request) { + return deletetoken(request, null); + } + + public CompletableFuture> deletetoken( + V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/delete") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBody.string(), V1FlowDeleteTokenResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> detokenize() { + return detokenize(V1FlowDetokenizeRequest.builder().build()); + } + + public CompletableFuture> detokenize( + V1FlowDetokenizeRequest request) { + return detokenize(request, null); + } + + public CompletableFuture> detokenize( + V1FlowDetokenizeRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/detokenize") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBody.string(), V1FlowDetokenizeResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> tokenize() { + return tokenize(V1FlowTokenizeRequest.builder().build()); + } + + public CompletableFuture> tokenize(V1FlowTokenizeRequest request) { + return tokenize(request, null); + } + + public CompletableFuture> tokenize( + V1FlowTokenizeRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/tokenize") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBody.string(), V1FlowTokenizeResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> flowvaultmetrics() { + return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build()); + } + + public CompletableFuture> flowvaultmetrics( + V1FlowVaultMetricsRequest request) { + return flowvaultmetrics(request, null); + } + + public CompletableFuture> flowvaultmetrics( + V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/vaults/metrics") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBody.string(), V1FlowVaultMetricsResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java new file mode 100644 index 00000000..8da9c9a5 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/FlowserviceClient.java @@ -0,0 +1,124 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.resources.flowservice.requests.*; +import com.skyflow.generated.rest.types.*; + +public class FlowserviceClient { + protected final ClientOptions clientOptions; + + private final RawFlowserviceClient rawClient; + + public FlowserviceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawFlowserviceClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawFlowserviceClient withRawResponse() { + return this.rawClient; + } + + public V1DeleteResponse delete() { + return this.rawClient.delete().body(); + } + + public V1DeleteResponse delete(V1DeleteRequest request) { + return this.rawClient.delete(request).body(); + } + + public V1DeleteResponse delete(V1DeleteRequest request, RequestOptions requestOptions) { + return this.rawClient.delete(request, requestOptions).body(); + } + + public V1GetResponse get() { + return this.rawClient.get().body(); + } + + public V1GetResponse get(V1GetRequest request) { + return this.rawClient.get(request).body(); + } + + public V1GetResponse get(V1GetRequest request, RequestOptions requestOptions) { + return this.rawClient.get(request, requestOptions).body(); + } + + public V1InsertResponse insert() { + return this.rawClient.insert().body(); + } + + public V1InsertResponse insert(V1InsertRequest request) { + return this.rawClient.insert(request).body(); + } + + public V1InsertResponse insert(V1InsertRequest request, RequestOptions requestOptions) { + return this.rawClient.insert(request, requestOptions).body(); + } + + public V1UpdateResponse update() { + return this.rawClient.update().body(); + } + + public V1UpdateResponse update(V1UpdateRequest request) { + return this.rawClient.update(request).body(); + } + + public V1UpdateResponse update(V1UpdateRequest request, RequestOptions requestOptions) { + return this.rawClient.update(request, requestOptions).body(); + } + + public V1FlowDeleteTokenResponse deletetoken() { + return this.rawClient.deletetoken().body(); + } + + public V1FlowDeleteTokenResponse deletetoken(V1FlowDeleteTokenRequest request) { + return this.rawClient.deletetoken(request).body(); + } + + public V1FlowDeleteTokenResponse deletetoken(V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { + return this.rawClient.deletetoken(request, requestOptions).body(); + } + + public V1FlowDetokenizeResponse detokenize() { + return this.rawClient.detokenize().body(); + } + + public V1FlowDetokenizeResponse detokenize(V1FlowDetokenizeRequest request) { + return this.rawClient.detokenize(request).body(); + } + + public V1FlowDetokenizeResponse detokenize(V1FlowDetokenizeRequest request, RequestOptions requestOptions) { + return this.rawClient.detokenize(request, requestOptions).body(); + } + + public V1FlowTokenizeResponse tokenize() { + return this.rawClient.tokenize().body(); + } + + public V1FlowTokenizeResponse tokenize(V1FlowTokenizeRequest request) { + return this.rawClient.tokenize(request).body(); + } + + public V1FlowTokenizeResponse tokenize(V1FlowTokenizeRequest request, RequestOptions requestOptions) { + return this.rawClient.tokenize(request, requestOptions).body(); + } + + public V1FlowVaultMetricsResponse flowvaultmetrics() { + return this.rawClient.flowvaultmetrics().body(); + } + + public V1FlowVaultMetricsResponse flowvaultmetrics(V1FlowVaultMetricsRequest request) { + return this.rawClient.flowvaultmetrics(request).body(); + } + + public V1FlowVaultMetricsResponse flowvaultmetrics( + V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { + return this.rawClient.flowvaultmetrics(request, requestOptions).body(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java new file mode 100644 index 00000000..f1203085 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/RawFlowserviceClient.java @@ -0,0 +1,412 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.skyflow.generated.rest.core.*; +import com.skyflow.generated.rest.resources.flowservice.requests.*; +import com.skyflow.generated.rest.types.*; +import okhttp3.*; + +import java.io.IOException; + +public class RawFlowserviceClient { + protected final ClientOptions clientOptions; + + public RawFlowserviceClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + public ApiClientHttpResponse delete() { + return delete(V1DeleteRequest.builder().build()); + } + + public ApiClientHttpResponse delete(V1DeleteRequest request) { + return delete(request, null); + } + + public ApiClientHttpResponse delete(V1DeleteRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/delete") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1DeleteResponse.class), response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse get() { + return get(V1GetRequest.builder().build()); + } + + public ApiClientHttpResponse get(V1GetRequest request) { + return get(request, null); + } + + public ApiClientHttpResponse get(V1GetRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/get") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1GetResponse.class), response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse insert() { + return insert(V1InsertRequest.builder().build()); + } + + public ApiClientHttpResponse insert(V1InsertRequest request) { + return insert(request, null); + } + + public ApiClientHttpResponse insert(V1InsertRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/insert") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1InsertResponse.class), response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse update() { + return update(V1UpdateRequest.builder().build()); + } + + public ApiClientHttpResponse update(V1UpdateRequest request) { + return update(request, null); + } + + public ApiClientHttpResponse update(V1UpdateRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/records/update") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1UpdateResponse.class), response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse deletetoken() { + return deletetoken(V1FlowDeleteTokenRequest.builder().build()); + } + + public ApiClientHttpResponse deletetoken(V1FlowDeleteTokenRequest request) { + return deletetoken(request, null); + } + + public ApiClientHttpResponse deletetoken( + V1FlowDeleteTokenRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/delete") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowDeleteTokenResponse.class), + response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse detokenize() { + return detokenize(V1FlowDetokenizeRequest.builder().build()); + } + + public ApiClientHttpResponse detokenize(V1FlowDetokenizeRequest request) { + return detokenize(request, null); + } + + public ApiClientHttpResponse detokenize( + V1FlowDetokenizeRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/detokenize") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowDetokenizeResponse.class), + response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse tokenize() { + return tokenize(V1FlowTokenizeRequest.builder().build()); + } + + public ApiClientHttpResponse tokenize(V1FlowTokenizeRequest request) { + return tokenize(request, null); + } + + public ApiClientHttpResponse tokenize( + V1FlowTokenizeRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/tokens/tokenize") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowTokenizeResponse.class), + response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } + + public ApiClientHttpResponse flowvaultmetrics() { + return flowvaultmetrics(V1FlowVaultMetricsRequest.builder().build()); + } + + public ApiClientHttpResponse flowvaultmetrics(V1FlowVaultMetricsRequest request) { + return flowvaultmetrics(request, null); + } + + public ApiClientHttpResponse flowvaultmetrics( + V1FlowVaultMetricsRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/vaults/metrics") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1FlowVaultMetricsResponse.class), + response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java new file mode 100644 index 00000000..27e22a8d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1DeleteRequest.java @@ -0,0 +1,186 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1UniqueValue; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1DeleteRequest.Builder.class) +public final class V1DeleteRequest { + private final Optional vaultId; + + private final Optional tableName; + + private final Optional> skyflowIDs; + + private final Optional> uniqueValues; + + private final Map additionalProperties; + + private V1DeleteRequest( + Optional vaultId, + Optional tableName, + Optional> skyflowIDs, + Optional> uniqueValues, + Map additionalProperties) { + this.vaultId = vaultId; + this.tableName = tableName; + this.skyflowIDs = skyflowIDs; + this.uniqueValues = uniqueValues; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where data is being deleted + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Name of the table where data is being deleted + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + /** + * @return Skyflow ID for the record to be deleted + */ + @JsonProperty("skyflowIDs") + public Optional> getSkyflowIDs() { + return skyflowIDs; + } + + /** + * @return List of unique constraint values to query records by data + */ + @JsonProperty("uniqueValues") + public Optional> getUniqueValues() { + return uniqueValues; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1DeleteRequest && equalTo((V1DeleteRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1DeleteRequest other) { + return vaultId.equals(other.vaultId) + && tableName.equals(other.tableName) + && skyflowIDs.equals(other.skyflowIDs) + && uniqueValues.equals(other.uniqueValues); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.tableName, this.skyflowIDs, this.uniqueValues); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional tableName = Optional.empty(); + + private Optional> skyflowIDs = Optional.empty(); + + private Optional> uniqueValues = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1DeleteRequest other) { + vaultId(other.getVaultId()); + tableName(other.getTableName()); + skyflowIDs(other.getSkyflowIDs()); + uniqueValues(other.getUniqueValues()); + return this; + } + + /** + *

ID of the vault where data is being deleted

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Name of the table where data is being deleted

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + /** + *

Skyflow ID for the record to be deleted

+ */ + @JsonSetter(value = "skyflowIDs", nulls = Nulls.SKIP) + public Builder skyflowIDs(Optional> skyflowIDs) { + this.skyflowIDs = skyflowIDs; + return this; + } + + public Builder skyflowIDs(List skyflowIDs) { + this.skyflowIDs = Optional.ofNullable(skyflowIDs); + return this; + } + + /** + *

List of unique constraint values to query records by data

+ */ + @JsonSetter(value = "uniqueValues", nulls = Nulls.SKIP) + public Builder uniqueValues(Optional> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public Builder uniqueValues(List uniqueValues) { + this.uniqueValues = Optional.ofNullable(uniqueValues); + return this; + } + + public V1DeleteRequest build() { + return new V1DeleteRequest(vaultId, tableName, skyflowIDs, uniqueValues, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java new file mode 100644 index 00000000..6bc8a4e6 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDeleteTokenRequest.java @@ -0,0 +1,122 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowDeleteTokenRequest.Builder.class) +public final class V1FlowDeleteTokenRequest { + private final Optional vaultId; + + private final Optional> tokens; + + private final Map additionalProperties; + + private V1FlowDeleteTokenRequest( + Optional vaultId, Optional> tokens, Map additionalProperties) { + this.vaultId = vaultId; + this.tokens = tokens; + this.additionalProperties = additionalProperties; + } + + /** + * @return Vault ID + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Token value + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowDeleteTokenRequest && equalTo((V1FlowDeleteTokenRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowDeleteTokenRequest other) { + return vaultId.equals(other.vaultId) && tokens.equals(other.tokens); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.tokens); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowDeleteTokenRequest other) { + vaultId(other.getVaultId()); + tokens(other.getTokens()); + return this; + } + + /** + *

Vault ID

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Token value

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(List tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + public V1FlowDeleteTokenRequest build() { + return new V1FlowDeleteTokenRequest(vaultId, tokens, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java new file mode 100644 index 00000000..bdbe3bf2 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowDetokenizeRequest.java @@ -0,0 +1,156 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowDetokenizeRequest.Builder.class) +public final class V1FlowDetokenizeRequest { + private final Optional vaultId; + + private final Optional> tokens; + + private final Optional> tokenGroupRedactions; + + private final Map additionalProperties; + + private V1FlowDetokenizeRequest( + Optional vaultId, + Optional> tokens, + Optional> tokenGroupRedactions, + Map additionalProperties) { + this.vaultId = vaultId; + this.tokens = tokens; + this.tokenGroupRedactions = tokenGroupRedactions; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where detokenizing + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Token to be detokenized + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + /** + * @return List of token groups to be redacted. + */ + @JsonProperty("tokenGroupRedactions") + public Optional> getTokenGroupRedactions() { + return tokenGroupRedactions; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowDetokenizeRequest && equalTo((V1FlowDetokenizeRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowDetokenizeRequest other) { + return vaultId.equals(other.vaultId) + && tokens.equals(other.tokens) + && tokenGroupRedactions.equals(other.tokenGroupRedactions); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.tokens, this.tokenGroupRedactions); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + private Optional> tokenGroupRedactions = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowDetokenizeRequest other) { + vaultId(other.getVaultId()); + tokens(other.getTokens()); + tokenGroupRedactions(other.getTokenGroupRedactions()); + return this; + } + + /** + *

ID of the vault where detokenizing

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Token to be detokenized

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(List tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + /** + *

List of token groups to be redacted.

+ */ + @JsonSetter(value = "tokenGroupRedactions", nulls = Nulls.SKIP) + public Builder tokenGroupRedactions(Optional> tokenGroupRedactions) { + this.tokenGroupRedactions = tokenGroupRedactions; + return this; + } + + public Builder tokenGroupRedactions(List tokenGroupRedactions) { + this.tokenGroupRedactions = Optional.ofNullable(tokenGroupRedactions); + return this; + } + + public V1FlowDetokenizeRequest build() { + return new V1FlowDetokenizeRequest(vaultId, tokens, tokenGroupRedactions, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java new file mode 100644 index 00000000..a27d7b62 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowTokenizeRequest.java @@ -0,0 +1,125 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowTokenizeRequest.Builder.class) +public final class V1FlowTokenizeRequest { + private final Optional vaultId; + + private final Optional> data; + + private final Map additionalProperties; + + private V1FlowTokenizeRequest( + Optional vaultId, + Optional> data, + Map additionalProperties) { + this.vaultId = vaultId; + this.data = data; + this.additionalProperties = additionalProperties; + } + + /** + * @return Vault ID. + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Data to be tokenized + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowTokenizeRequest && equalTo((V1FlowTokenizeRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowTokenizeRequest other) { + return vaultId.equals(other.vaultId) && data.equals(other.data); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.data); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional> data = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowTokenizeRequest other) { + vaultId(other.getVaultId()); + data(other.getData()); + return this; + } + + /** + *

Vault ID.

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Data to be tokenized

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(List data) { + this.data = Optional.ofNullable(data); + return this; + } + + public V1FlowTokenizeRequest build() { + return new V1FlowTokenizeRequest(vaultId, data, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java new file mode 100644 index 00000000..026d2d30 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1FlowVaultMetricsRequest.java @@ -0,0 +1,96 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowVaultMetricsRequest.Builder.class) +public final class V1FlowVaultMetricsRequest { + private final Optional vaultId; + + private final Map additionalProperties; + + private V1FlowVaultMetricsRequest(Optional vaultId, Map additionalProperties) { + this.vaultId = vaultId; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault to get metrics for + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowVaultMetricsRequest && equalTo((V1FlowVaultMetricsRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowVaultMetricsRequest other) { + return vaultId.equals(other.vaultId); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowVaultMetricsRequest other) { + vaultId(other.getVaultId()); + return this; + } + + /** + *

ID of the vault to get metrics for

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + public V1FlowVaultMetricsRequest build() { + return new V1FlowVaultMetricsRequest(vaultId, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java new file mode 100644 index 00000000..415f5291 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1GetRequest.java @@ -0,0 +1,357 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1ColumnRedactions; +import com.skyflow.generated.rest.types.V1GetRequestData; +import com.skyflow.generated.rest.types.V1UniqueValue; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1GetRequest.Builder.class) +public final class V1GetRequest { + private final Optional vaultId; + + private final Optional tableName; + + private final Optional> skyflowIDs; + + private final Optional> columnRedactions; + + private final Optional> columns; + + private final Optional limit; + + private final Optional offset; + + private final Optional> uniqueValues; + + private final Optional> records; + + private final Map additionalProperties; + + private V1GetRequest( + Optional vaultId, + Optional tableName, + Optional> skyflowIDs, + Optional> columnRedactions, + Optional> columns, + Optional limit, + Optional offset, + Optional> uniqueValues, + Optional> records, + Map additionalProperties) { + this.vaultId = vaultId; + this.tableName = tableName; + this.skyflowIDs = skyflowIDs; + this.columnRedactions = columnRedactions; + this.columns = columns; + this.limit = limit; + this.offset = offset; + this.uniqueValues = uniqueValues; + this.records = records; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where data is being fetched + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Name of the table where data is being fetched + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + /** + * @return Skyflow ID for the record to be fetched + */ + @JsonProperty("skyflowIDs") + public Optional> getSkyflowIDs() { + return skyflowIDs; + } + + /** + * @return List of columns to be redacted. + */ + @JsonProperty("columnRedactions") + public Optional> getColumnRedactions() { + return columnRedactions; + } + + /** + * @return List of columns to be fetched. + */ + @JsonProperty("columns") + public Optional> getColumns() { + return columns; + } + + /** + * @return Limit for the number of records to be fetched + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + /** + * @return Offset for the number of records to be fetched + */ + @JsonProperty("offset") + public Optional getOffset() { + return offset; + } + + /** + * @return List of unique constraint values to query records by data + */ + @JsonProperty("uniqueValues") + public Optional> getUniqueValues() { + return uniqueValues; + } + + /** + * @return List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table. + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1GetRequest && equalTo((V1GetRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1GetRequest other) { + return vaultId.equals(other.vaultId) + && tableName.equals(other.tableName) + && skyflowIDs.equals(other.skyflowIDs) + && columnRedactions.equals(other.columnRedactions) + && columns.equals(other.columns) + && limit.equals(other.limit) + && offset.equals(other.offset) + && uniqueValues.equals(other.uniqueValues) + && records.equals(other.records); + } + + @Override + public int hashCode() { + return Objects.hash( + this.vaultId, + this.tableName, + this.skyflowIDs, + this.columnRedactions, + this.columns, + this.limit, + this.offset, + this.uniqueValues, + this.records); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional tableName = Optional.empty(); + + private Optional> skyflowIDs = Optional.empty(); + + private Optional> columnRedactions = Optional.empty(); + + private Optional> columns = Optional.empty(); + + private Optional limit = Optional.empty(); + + private Optional offset = Optional.empty(); + + private Optional> uniqueValues = Optional.empty(); + + private Optional> records = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1GetRequest other) { + vaultId(other.getVaultId()); + tableName(other.getTableName()); + skyflowIDs(other.getSkyflowIDs()); + columnRedactions(other.getColumnRedactions()); + columns(other.getColumns()); + limit(other.getLimit()); + offset(other.getOffset()); + uniqueValues(other.getUniqueValues()); + records(other.getRecords()); + return this; + } + + /** + *

ID of the vault where data is being fetched

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Name of the table where data is being fetched

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + /** + *

Skyflow ID for the record to be fetched

+ */ + @JsonSetter(value = "skyflowIDs", nulls = Nulls.SKIP) + public Builder skyflowIDs(Optional> skyflowIDs) { + this.skyflowIDs = skyflowIDs; + return this; + } + + public Builder skyflowIDs(List skyflowIDs) { + this.skyflowIDs = Optional.ofNullable(skyflowIDs); + return this; + } + + /** + *

List of columns to be redacted.

+ */ + @JsonSetter(value = "columnRedactions", nulls = Nulls.SKIP) + public Builder columnRedactions(Optional> columnRedactions) { + this.columnRedactions = columnRedactions; + return this; + } + + public Builder columnRedactions(List columnRedactions) { + this.columnRedactions = Optional.ofNullable(columnRedactions); + return this; + } + + /** + *

List of columns to be fetched.

+ */ + @JsonSetter(value = "columns", nulls = Nulls.SKIP) + public Builder columns(Optional> columns) { + this.columns = columns; + return this; + } + + public Builder columns(List columns) { + this.columns = Optional.ofNullable(columns); + return this; + } + + /** + *

Limit for the number of records to be fetched

+ */ + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + /** + *

Offset for the number of records to be fetched

+ */ + @JsonSetter(value = "offset", nulls = Nulls.SKIP) + public Builder offset(Optional offset) { + this.offset = offset; + return this; + } + + public Builder offset(Integer offset) { + this.offset = Optional.ofNullable(offset); + return this; + } + + /** + *

List of unique constraint values to query records by data

+ */ + @JsonSetter(value = "uniqueValues", nulls = Nulls.SKIP) + public Builder uniqueValues(Optional> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public Builder uniqueValues(List uniqueValues) { + this.uniqueValues = Optional.ofNullable(uniqueValues); + return this; + } + + /** + *

List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table.

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + public V1GetRequest build() { + return new V1GetRequest( + vaultId, + tableName, + skyflowIDs, + columnRedactions, + columns, + limit, + offset, + uniqueValues, + records, + additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java new file mode 100644 index 00000000..47a58cb2 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1InsertRequest.java @@ -0,0 +1,181 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1Upsert; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1InsertRequest.Builder.class) +public final class V1InsertRequest { + private final Optional vaultId; + + private final Optional tableName; + + private final Optional> records; + + private final Optional upsert; + + private final Map additionalProperties; + + private V1InsertRequest( + Optional vaultId, + Optional tableName, + Optional> records, + Optional upsert, + Map additionalProperties) { + this.vaultId = vaultId; + this.tableName = tableName; + this.records = records; + this.upsert = upsert; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where data is being inserted + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Name of the table where data is being inserted + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + /** + * @return List of data row wise that is to be inserted in the vault + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @JsonProperty("upsert") + public Optional getUpsert() { + return upsert; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1InsertRequest && equalTo((V1InsertRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1InsertRequest other) { + return vaultId.equals(other.vaultId) + && tableName.equals(other.tableName) + && records.equals(other.records) + && upsert.equals(other.upsert); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.tableName, this.records, this.upsert); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional tableName = Optional.empty(); + + private Optional> records = Optional.empty(); + + private Optional upsert = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1InsertRequest other) { + vaultId(other.getVaultId()); + tableName(other.getTableName()); + records(other.getRecords()); + upsert(other.getUpsert()); + return this; + } + + /** + *

ID of the vault where data is being inserted

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Name of the table where data is being inserted

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + /** + *

List of data row wise that is to be inserted in the vault

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + @JsonSetter(value = "upsert", nulls = Nulls.SKIP) + public Builder upsert(Optional upsert) { + this.upsert = upsert; + return this; + } + + public Builder upsert(V1Upsert upsert) { + this.upsert = Optional.ofNullable(upsert); + return this; + } + + public V1InsertRequest build() { + return new V1InsertRequest(vaultId, tableName, records, upsert, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java new file mode 100644 index 00000000..6f8b303e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/flowservice/requests/V1UpdateRequest.java @@ -0,0 +1,181 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.flowservice.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.V1UpdateRecordData; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1UpdateRequest.Builder.class) +public final class V1UpdateRequest { + private final Optional vaultId; + + private final Optional tableName; + + private final Optional> records; + + private final Optional updateType; + + private final Map additionalProperties; + + private V1UpdateRequest( + Optional vaultId, + Optional tableName, + Optional> records, + Optional updateType, + Map additionalProperties) { + this.vaultId = vaultId; + this.tableName = tableName; + this.records = records; + this.updateType = updateType; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where data is being updated + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Name of the table where data is being updated + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + /** + * @return List of data row wise that is to be updated in the vault + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @JsonProperty("updateType") + public Optional getUpdateType() { + return updateType; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1UpdateRequest && equalTo((V1UpdateRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1UpdateRequest other) { + return vaultId.equals(other.vaultId) + && tableName.equals(other.tableName) + && records.equals(other.records) + && updateType.equals(other.updateType); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.tableName, this.records, this.updateType); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional tableName = Optional.empty(); + + private Optional> records = Optional.empty(); + + private Optional updateType = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1UpdateRequest other) { + vaultId(other.getVaultId()); + tableName(other.getTableName()); + records(other.getRecords()); + updateType(other.getUpdateType()); + return this; + } + + /** + *

ID of the vault where data is being updated

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Name of the table where data is being updated

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + /** + *

List of data row wise that is to be updated in the vault

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + @JsonSetter(value = "updateType", nulls = Nulls.SKIP) + public Builder updateType(Optional updateType) { + this.updateType = updateType; + return this; + } + + public Builder updateType(FlowEnumUpdateType updateType) { + this.updateType = Optional.ofNullable(updateType); + return this; + } + + public V1UpdateRequest build() { + return new V1UpdateRequest(vaultId, tableName, records, updateType, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java new file mode 100644 index 00000000..c91ad763 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java @@ -0,0 +1,96 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.records; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.skyflow.generated.rest.core.*; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public class AsyncRawRecordsClient { + protected final ClientOptions clientOptions; + + public AsyncRawRecordsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture> flowServiceExecuteQuery() { + return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build()); + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture> flowServiceExecuteQuery( + V1ExecuteQueryRequest request) { + return flowServiceExecuteQuery(request, null); + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture> flowServiceExecuteQuery( + V1ExecuteQueryRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/query") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBody.string(), V1ExecuteQueryResponse.class), + response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + future.completeExceptionally(new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } catch (IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new ApiClientException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java new file mode 100644 index 00000000..9af54869 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java @@ -0,0 +1,51 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.records; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; + +import java.util.concurrent.CompletableFuture; + +public class AsyncRecordsClient { + protected final ClientOptions clientOptions; + + private final AsyncRawRecordsClient rawClient; + + public AsyncRecordsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawRecordsClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawRecordsClient withRawResponse() { + return this.rawClient; + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture flowServiceExecuteQuery() { + return this.rawClient.flowServiceExecuteQuery().thenApply(response -> response.body()); + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture flowServiceExecuteQuery(V1ExecuteQueryRequest request) { + return this.rawClient.flowServiceExecuteQuery(request).thenApply(response -> response.body()); + } + + /** + * Executes a query on the specified vault. + */ + public CompletableFuture flowServiceExecuteQuery( + V1ExecuteQueryRequest request, RequestOptions requestOptions) { + return this.rawClient.flowServiceExecuteQuery(request, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java new file mode 100644 index 00000000..cf9a1db1 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java @@ -0,0 +1,79 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.records; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.skyflow.generated.rest.core.*; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; +import okhttp3.*; + +import java.io.IOException; + +public class RawRecordsClient { + protected final ClientOptions clientOptions; + + public RawRecordsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Executes a query on the specified vault. + */ + public ApiClientHttpResponse flowServiceExecuteQuery() { + return flowServiceExecuteQuery(V1ExecuteQueryRequest.builder().build()); + } + + /** + * Executes a query on the specified vault. + */ + public ApiClientHttpResponse flowServiceExecuteQuery(V1ExecuteQueryRequest request) { + return flowServiceExecuteQuery(request, null); + } + + /** + * Executes a query on the specified vault. + */ + public ApiClientHttpResponse flowServiceExecuteQuery( + V1ExecuteQueryRequest request, RequestOptions requestOptions) { + HttpUrl httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("v2/query") + .build(); + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new ApiClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new ApiClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBody.string(), V1ExecuteQueryResponse.class), + response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + throw new ApiClientApiException( + "Error with status code " + response.code(), + response.code(), + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response); + } catch (IOException e) { + throw new ApiClientException("Network error executing HTTP request", e); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java new file mode 100644 index 00000000..332c0e1b --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java @@ -0,0 +1,49 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.records; + +import com.skyflow.generated.rest.core.ClientOptions; +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.resources.records.requests.V1ExecuteQueryRequest; +import com.skyflow.generated.rest.types.V1ExecuteQueryResponse; + +public class RecordsClient { + protected final ClientOptions clientOptions; + + private final RawRecordsClient rawClient; + + public RecordsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawRecordsClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawRecordsClient withRawResponse() { + return this.rawClient; + } + + /** + * Executes a query on the specified vault. + */ + public V1ExecuteQueryResponse flowServiceExecuteQuery() { + return this.rawClient.flowServiceExecuteQuery().body(); + } + + /** + * Executes a query on the specified vault. + */ + public V1ExecuteQueryResponse flowServiceExecuteQuery(V1ExecuteQueryRequest request) { + return this.rawClient.flowServiceExecuteQuery(request).body(); + } + + /** + * Executes a query on the specified vault. + */ + public V1ExecuteQueryResponse flowServiceExecuteQuery( + V1ExecuteQueryRequest request, RequestOptions requestOptions) { + return this.rawClient.flowServiceExecuteQuery(request, requestOptions).body(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java new file mode 100644 index 00000000..2857faca --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/resources/records/requests/V1ExecuteQueryRequest.java @@ -0,0 +1,125 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.resources.records.requests; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1ExecuteQueryRequest.Builder.class) +public final class V1ExecuteQueryRequest { + private final Optional vaultId; + + private final Optional query; + + private final Map additionalProperties; + + private V1ExecuteQueryRequest( + Optional vaultId, Optional query, Map additionalProperties) { + this.vaultId = vaultId; + this.query = query; + this.additionalProperties = additionalProperties; + } + + /** + * @return ID of the vault where data is being inserted + */ + @JsonProperty("vaultID") + public Optional getVaultId() { + return vaultId; + } + + /** + * @return Query to execute. + */ + @JsonProperty("query") + public Optional getQuery() { + return query; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1ExecuteQueryRequest && equalTo((V1ExecuteQueryRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1ExecuteQueryRequest other) { + return vaultId.equals(other.vaultId) && query.equals(other.query); + } + + @Override + public int hashCode() { + return Objects.hash(this.vaultId, this.query); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional vaultId = Optional.empty(); + + private Optional query = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1ExecuteQueryRequest other) { + vaultId(other.getVaultId()); + query(other.getQuery()); + return this; + } + + /** + *

ID of the vault where data is being inserted

+ */ + @JsonSetter(value = "vaultID", nulls = Nulls.SKIP) + public Builder vaultId(Optional vaultId) { + this.vaultId = vaultId; + return this; + } + + public Builder vaultId(String vaultId) { + this.vaultId = Optional.ofNullable(vaultId); + return this; + } + + /** + *

Query to execute.

+ */ + @JsonSetter(value = "query", nulls = Nulls.SKIP) + public Builder query(Optional query) { + this.query = query; + return this; + } + + public Builder query(String query) { + this.query = Optional.ofNullable(query); + return this; + } + + public V1ExecuteQueryRequest build() { + return new V1ExecuteQueryRequest(vaultId, query, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java new file mode 100644 index 00000000..d3c1cea0 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowEnumUpdateType.java @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum FlowEnumUpdateType { + UPDATE("UPDATE"), + + REPLACE("REPLACE"); + + private final String value; + + FlowEnumUpdateType(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java new file mode 100644 index 00000000..0edfd9a8 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/FlowTokenizeResponseObjectToken.java @@ -0,0 +1,188 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = FlowTokenizeResponseObjectToken.Builder.class) +public final class FlowTokenizeResponseObjectToken { + private final Optional tokenGroupName; + + private final Optional token; + + private final Optional error; + + private final Optional httpCode; + + private final Map additionalProperties; + + private FlowTokenizeResponseObjectToken( + Optional tokenGroupName, + Optional token, + Optional error, + Optional httpCode, + Map additionalProperties) { + this.tokenGroupName = tokenGroupName; + this.token = token; + this.error = error; + this.httpCode = httpCode; + this.additionalProperties = additionalProperties; + } + + /** + * @return Token group Name + */ + @JsonProperty("tokenGroupName") + public Optional getTokenGroupName() { + return tokenGroupName; + } + + /** + * @return Token value + */ + @JsonProperty("token") + public Optional getToken() { + return token; + } + + /** + * @return Error if tokenization failed + */ + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof FlowTokenizeResponseObjectToken && equalTo((FlowTokenizeResponseObjectToken) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(FlowTokenizeResponseObjectToken other) { + return tokenGroupName.equals(other.tokenGroupName) + && token.equals(other.token) + && error.equals(other.error) + && httpCode.equals(other.httpCode); + } + + @Override + public int hashCode() { + return Objects.hash(this.tokenGroupName, this.token, this.error, this.httpCode); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional tokenGroupName = Optional.empty(); + + private Optional token = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(FlowTokenizeResponseObjectToken other) { + tokenGroupName(other.getTokenGroupName()); + token(other.getToken()); + error(other.getError()); + httpCode(other.getHttpCode()); + return this; + } + + /** + *

Token group Name

+ */ + @JsonSetter(value = "tokenGroupName", nulls = Nulls.SKIP) + public Builder tokenGroupName(Optional tokenGroupName) { + this.tokenGroupName = tokenGroupName; + return this; + } + + public Builder tokenGroupName(String tokenGroupName) { + this.tokenGroupName = Optional.ofNullable(tokenGroupName); + return this; + } + + /** + *

Token value

+ */ + @JsonSetter(value = "token", nulls = Nulls.SKIP) + public Builder token(Optional token) { + this.token = token; + return this; + } + + public Builder token(String token) { + this.token = Optional.ofNullable(token); + return this; + } + + /** + *

Error if tokenization failed

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

+ */ + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; + return this; + } + + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); + return this; + } + + public FlowTokenizeResponseObjectToken build() { + return new FlowTokenizeResponseObjectToken(tokenGroupName, token, error, httpCode, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java new file mode 100644 index 00000000..06205978 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/GoogleprotobufAny.java @@ -0,0 +1,146 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GoogleprotobufAny.Builder.class) +public final class GoogleprotobufAny { + private final Optional type; + + private final Map additionalProperties; + + private GoogleprotobufAny(Optional type, Map additionalProperties) { + this.type = type; + this.additionalProperties = additionalProperties; + } + + /** + * @return A URL/resource name that uniquely identifies the type of the serialized + * protocol buffer message. This string must contain at least + * one "/" character. The last segment of the URL's path must represent + * the fully qualified name of the type (as in + * path/google.protobuf.Duration). The name should be in a canonical form + * (e.g., leading "." is not accepted). + *

In practice, teams usually precompile into the binary all types that they + * expect it to use in the context of Any. However, for URLs which use the + * scheme http, https, or no scheme, one can optionally set up a type + * server that maps type URLs to message definitions as follows:

+ *
    + *
  • If no scheme is provided, https is assumed.
  • + *
  • An HTTP GET on the URL must yield a [google.protobuf.Type][] + * value in binary format, or produce an error.
  • + *
  • Applications are allowed to cache lookup results based on the + * URL, or have them precompiled into a binary to avoid any + * lookup. Therefore, binary compatibility needs to be preserved + * on changes to types. (Use versioned type names to manage + * breaking changes.)
  • + *
+ *

Note: this functionality is not currently available in the official + * protobuf release, and it is not used for type URLs beginning with + * type.googleapis.com. As of May 2023, there are no widely used type server + * implementations and no plans to implement one.

+ *

Schemes other than http, https (or the empty scheme) might be + * used with implementation specific semantics.

+ */ + @JsonProperty("@type") + public Optional getType() { + return type; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GoogleprotobufAny && equalTo((GoogleprotobufAny) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(GoogleprotobufAny other) { + return type.equals(other.type); + } + + @Override + public int hashCode() { + return Objects.hash(this.type); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional type = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GoogleprotobufAny other) { + type(other.getType()); + return this; + } + + /** + *

A URL/resource name that uniquely identifies the type of the serialized + * protocol buffer message. This string must contain at least + * one "/" character. The last segment of the URL's path must represent + * the fully qualified name of the type (as in + * path/google.protobuf.Duration). The name should be in a canonical form + * (e.g., leading "." is not accepted).

+ *

In practice, teams usually precompile into the binary all types that they + * expect it to use in the context of Any. However, for URLs which use the + * scheme http, https, or no scheme, one can optionally set up a type + * server that maps type URLs to message definitions as follows:

+ *
    + *
  • If no scheme is provided, https is assumed.
  • + *
  • An HTTP GET on the URL must yield a [google.protobuf.Type][] + * value in binary format, or produce an error.
  • + *
  • Applications are allowed to cache lookup results based on the + * URL, or have them precompiled into a binary to avoid any + * lookup. Therefore, binary compatibility needs to be preserved + * on changes to types. (Use versioned type names to manage + * breaking changes.)
  • + *
+ *

Note: this functionality is not currently available in the official + * protobuf release, and it is not used for type URLs beginning with + * type.googleapis.com. As of May 2023, there are no widely used type server + * implementations and no plans to implement one.

+ *

Schemes other than http, https (or the empty scheme) might be + * used with implementation specific semantics.

+ */ + @JsonSetter(value = "@type", nulls = Nulls.SKIP) + public Builder type(Optional type) { + this.type = type; + return this; + } + + public Builder type(String type) { + this.type = Optional.ofNullable(type); + return this; + } + + public GoogleprotobufAny build() { + return new GoogleprotobufAny(type, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java new file mode 100644 index 00000000..347ce5c4 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/RpcStatus.java @@ -0,0 +1,135 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = RpcStatus.Builder.class) +public final class RpcStatus { + private final Optional code; + + private final Optional message; + + private final Optional> details; + + private final Map additionalProperties; + + private RpcStatus( + Optional code, + Optional message, + Optional> details, + Map additionalProperties) { + this.code = code; + this.message = message; + this.details = details; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("code") + public Optional getCode() { + return code; + } + + @JsonProperty("message") + public Optional getMessage() { + return message; + } + + @JsonProperty("details") + public Optional> getDetails() { + return details; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof RpcStatus && equalTo((RpcStatus) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(RpcStatus other) { + return code.equals(other.code) && message.equals(other.message) && details.equals(other.details); + } + + @Override + public int hashCode() { + return Objects.hash(this.code, this.message, this.details); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional code = Optional.empty(); + + private Optional message = Optional.empty(); + + private Optional> details = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(RpcStatus other) { + code(other.getCode()); + message(other.getMessage()); + details(other.getDetails()); + return this; + } + + @JsonSetter(value = "code", nulls = Nulls.SKIP) + public Builder code(Optional code) { + this.code = code; + return this; + } + + public Builder code(Integer code) { + this.code = Optional.ofNullable(code); + return this; + } + + @JsonSetter(value = "message", nulls = Nulls.SKIP) + public Builder message(Optional message) { + this.message = message; + return this; + } + + public Builder message(String message) { + this.message = Optional.ofNullable(message); + return this; + } + + @JsonSetter(value = "details", nulls = Nulls.SKIP) + public Builder details(Optional> details) { + this.details = details; + return this; + } + + public Builder details(List details) { + this.details = Optional.ofNullable(details); + return this; + } + + public RpcStatus build() { + return new RpcStatus(code, message, details, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java new file mode 100644 index 00000000..b467a8c0 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ColumnRedactions.java @@ -0,0 +1,125 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1ColumnRedactions.Builder.class) +public final class V1ColumnRedactions { + private final Optional columnName; + + private final Optional redaction; + + private final Map additionalProperties; + + private V1ColumnRedactions( + Optional columnName, Optional redaction, Map additionalProperties) { + this.columnName = columnName; + this.redaction = redaction; + this.additionalProperties = additionalProperties; + } + + /** + * @return Name of the column to be redacted + */ + @JsonProperty("columnName") + public Optional getColumnName() { + return columnName; + } + + /** + * @return Name of the redaction. Eg: plain_text, redacted, mask1 + */ + @JsonProperty("redaction") + public Optional getRedaction() { + return redaction; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1ColumnRedactions && equalTo((V1ColumnRedactions) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1ColumnRedactions other) { + return columnName.equals(other.columnName) && redaction.equals(other.redaction); + } + + @Override + public int hashCode() { + return Objects.hash(this.columnName, this.redaction); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional columnName = Optional.empty(); + + private Optional redaction = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1ColumnRedactions other) { + columnName(other.getColumnName()); + redaction(other.getRedaction()); + return this; + } + + /** + *

Name of the column to be redacted

+ */ + @JsonSetter(value = "columnName", nulls = Nulls.SKIP) + public Builder columnName(Optional columnName) { + this.columnName = columnName; + return this; + } + + public Builder columnName(String columnName) { + this.columnName = Optional.ofNullable(columnName); + return this; + } + + /** + *

Name of the redaction. Eg: plain_text, redacted, mask1

+ */ + @JsonSetter(value = "redaction", nulls = Nulls.SKIP) + public Builder redaction(Optional redaction) { + this.redaction = redaction; + return this; + } + + public Builder redaction(String redaction) { + this.redaction = Optional.ofNullable(redaction); + return this; + } + + public V1ColumnRedactions build() { + return new V1ColumnRedactions(columnName, redaction, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java new file mode 100644 index 00000000..24bd865d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponse.java @@ -0,0 +1,93 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1DeleteResponse.Builder.class) +public final class V1DeleteResponse { + private final Optional> records; + + private final Map additionalProperties; + + private V1DeleteResponse(Optional> records, Map additionalProperties) { + this.records = records; + this.additionalProperties = additionalProperties; + } + + /** + * @return List of deleted records with skyflow ID and any partial errors. + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1DeleteResponse && equalTo((V1DeleteResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1DeleteResponse other) { + return records.equals(other.records); + } + + @Override + public int hashCode() { + return Objects.hash(this.records); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> records = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1DeleteResponse other) { + records(other.getRecords()); + return this; + } + + /** + *

List of deleted records with skyflow ID and any partial errors.

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + public V1DeleteResponse build() { + return new V1DeleteResponse(records, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java new file mode 100644 index 00000000..087ce4c7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteResponseObject.java @@ -0,0 +1,156 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1DeleteResponseObject.Builder.class) +public final class V1DeleteResponseObject { + private final Optional skyflowId; + + private final Optional error; + + private final Optional httpCode; + + private final Map additionalProperties; + + private V1DeleteResponseObject( + Optional skyflowId, + Optional error, + Optional httpCode, + Map additionalProperties) { + this.skyflowId = skyflowId; + this.error = error; + this.httpCode = httpCode; + this.additionalProperties = additionalProperties; + } + + /** + * @return Skyflow ID for the deleted record + */ + @JsonProperty("skyflowID") + public Optional getSkyflowId() { + return skyflowId; + } + + /** + * @return Partial Error message if any + */ + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1DeleteResponseObject && equalTo((V1DeleteResponseObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1DeleteResponseObject other) { + return skyflowId.equals(other.skyflowId) && error.equals(other.error) && httpCode.equals(other.httpCode); + } + + @Override + public int hashCode() { + return Objects.hash(this.skyflowId, this.error, this.httpCode); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional skyflowId = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1DeleteResponseObject other) { + skyflowId(other.getSkyflowId()); + error(other.getError()); + httpCode(other.getHttpCode()); + return this; + } + + /** + *

Skyflow ID for the deleted record

+ */ + @JsonSetter(value = "skyflowID", nulls = Nulls.SKIP) + public Builder skyflowId(Optional skyflowId) { + this.skyflowId = skyflowId; + return this; + } + + public Builder skyflowId(String skyflowId) { + this.skyflowId = Optional.ofNullable(skyflowId); + return this; + } + + /** + *

Partial Error message if any

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

+ */ + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; + return this; + } + + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); + return this; + } + + public V1DeleteResponseObject build() { + return new V1DeleteResponseObject(skyflowId, error, httpCode, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java new file mode 100644 index 00000000..f2e1dacd --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteTokenResponseObject.java @@ -0,0 +1,156 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1DeleteTokenResponseObject.Builder.class) +public final class V1DeleteTokenResponseObject { + private final Optional value; + + private final Optional error; + + private final Optional httpCode; + + private final Map additionalProperties; + + private V1DeleteTokenResponseObject( + Optional value, + Optional error, + Optional httpCode, + Map additionalProperties) { + this.value = value; + this.error = error; + this.httpCode = httpCode; + this.additionalProperties = additionalProperties; + } + + /** + * @return Token value + */ + @JsonProperty("value") + public Optional getValue() { + return value; + } + + /** + * @return Error if deletion failed + */ + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1DeleteTokenResponseObject && equalTo((V1DeleteTokenResponseObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1DeleteTokenResponseObject other) { + return value.equals(other.value) && error.equals(other.error) && httpCode.equals(other.httpCode); + } + + @Override + public int hashCode() { + return Objects.hash(this.value, this.error, this.httpCode); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional value = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1DeleteTokenResponseObject other) { + value(other.getValue()); + error(other.getError()); + httpCode(other.getHttpCode()); + return this; + } + + /** + *

Token value

+ */ + @JsonSetter(value = "value", nulls = Nulls.SKIP) + public Builder value(Optional value) { + this.value = value; + return this; + } + + public Builder value(String value) { + this.value = Optional.ofNullable(value); + return this; + } + + /** + *

Error if deletion failed

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

+ */ + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; + return this; + } + + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); + return this; + } + + public V1DeleteTokenResponseObject build() { + return new V1DeleteTokenResponseObject(value, error, httpCode, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java new file mode 100644 index 00000000..9de3418b --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryRecordResponse.java @@ -0,0 +1,96 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1ExecuteQueryRecordResponse.Builder.class) +public final class V1ExecuteQueryRecordResponse { + private final Optional> data; + + private final Map additionalProperties; + + private V1ExecuteQueryRecordResponse(Optional> data, Map additionalProperties) { + this.data = data; + this.additionalProperties = additionalProperties; + } + + /** + * @return Fields and values for the record. For example, {'field_1':'value_1', 'field_2':'value_2'}. + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1ExecuteQueryRecordResponse && equalTo((V1ExecuteQueryRecordResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1ExecuteQueryRecordResponse other) { + return data.equals(other.data); + } + + @Override + public int hashCode() { + return Objects.hash(this.data); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> data = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1ExecuteQueryRecordResponse other) { + data(other.getData()); + return this; + } + + /** + *

Fields and values for the record. For example, {'field_1':'value_1', 'field_2':'value_2'}.

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(Map data) { + this.data = Optional.ofNullable(data); + return this; + } + + public V1ExecuteQueryRecordResponse build() { + return new V1ExecuteQueryRecordResponse(data, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java new file mode 100644 index 00000000..f80b54e0 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponse.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1ExecuteQueryResponse.Builder.class) +public final class V1ExecuteQueryResponse { + private final Optional> records; + + private final Optional metadata; + + private final Map additionalProperties; + + private V1ExecuteQueryResponse( + Optional> records, + Optional metadata, + Map additionalProperties) { + this.records = records; + this.metadata = metadata; + this.additionalProperties = additionalProperties; + } + + /** + * @return Records corresponding to the specified query. + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @JsonProperty("metadata") + public Optional getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1ExecuteQueryResponse && equalTo((V1ExecuteQueryResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1ExecuteQueryResponse other) { + return records.equals(other.records) && metadata.equals(other.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.records, this.metadata); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> records = Optional.empty(); + + private Optional metadata = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1ExecuteQueryResponse other) { + records(other.getRecords()); + metadata(other.getMetadata()); + return this; + } + + /** + *

Records corresponding to the specified query.

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + @JsonSetter(value = "metadata", nulls = Nulls.SKIP) + public Builder metadata(Optional metadata) { + this.metadata = metadata; + return this; + } + + public Builder metadata(V1ExecuteQueryResponseMetadata metadata) { + this.metadata = Optional.ofNullable(metadata); + return this; + } + + public V1ExecuteQueryResponse build() { + return new V1ExecuteQueryResponse(records, metadata, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java new file mode 100644 index 00000000..c23da3bd --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1ExecuteQueryResponseMetadata.java @@ -0,0 +1,93 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1ExecuteQueryResponseMetadata.Builder.class) +public final class V1ExecuteQueryResponseMetadata { + private final Optional> columns; + + private final Map additionalProperties; + + private V1ExecuteQueryResponseMetadata(Optional> columns, Map additionalProperties) { + this.columns = columns; + this.additionalProperties = additionalProperties; + } + + /** + * @return Return columns for the query + */ + @JsonProperty("columns") + public Optional> getColumns() { + return columns; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1ExecuteQueryResponseMetadata && equalTo((V1ExecuteQueryResponseMetadata) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1ExecuteQueryResponseMetadata other) { + return columns.equals(other.columns); + } + + @Override + public int hashCode() { + return Objects.hash(this.columns); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> columns = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1ExecuteQueryResponseMetadata other) { + columns(other.getColumns()); + return this; + } + + /** + *

Return columns for the query

+ */ + @JsonSetter(value = "columns", nulls = Nulls.SKIP) + public Builder columns(Optional> columns) { + this.columns = columns; + return this; + } + + public Builder columns(List columns) { + this.columns = Optional.ofNullable(columns); + return this; + } + + public V1ExecuteQueryResponseMetadata build() { + return new V1ExecuteQueryResponseMetadata(columns, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java new file mode 100644 index 00000000..34ca245c --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDeleteTokenResponse.java @@ -0,0 +1,94 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowDeleteTokenResponse.Builder.class) +public final class V1FlowDeleteTokenResponse { + private final Optional> tokens; + + private final Map additionalProperties; + + private V1FlowDeleteTokenResponse( + Optional> tokens, Map additionalProperties) { + this.tokens = tokens; + this.additionalProperties = additionalProperties; + } + + /** + * @return Tokens data for Delete + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowDeleteTokenResponse && equalTo((V1FlowDeleteTokenResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowDeleteTokenResponse other) { + return tokens.equals(other.tokens); + } + + @Override + public int hashCode() { + return Objects.hash(this.tokens); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> tokens = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowDeleteTokenResponse other) { + tokens(other.getTokens()); + return this; + } + + /** + *

Tokens data for Delete

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(List tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + public V1FlowDeleteTokenResponse build() { + return new V1FlowDeleteTokenResponse(tokens, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java new file mode 100644 index 00000000..defe1dad --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponse.java @@ -0,0 +1,94 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowDetokenizeResponse.Builder.class) +public final class V1FlowDetokenizeResponse { + private final Optional> response; + + private final Map additionalProperties; + + private V1FlowDetokenizeResponse( + Optional> response, Map additionalProperties) { + this.response = response; + this.additionalProperties = additionalProperties; + } + + /** + * @return Detokenized data + */ + @JsonProperty("response") + public Optional> getResponse() { + return response; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowDetokenizeResponse && equalTo((V1FlowDetokenizeResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowDetokenizeResponse other) { + return response.equals(other.response); + } + + @Override + public int hashCode() { + return Objects.hash(this.response); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> response = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowDetokenizeResponse other) { + response(other.getResponse()); + return this; + } + + /** + *

Detokenized data

+ */ + @JsonSetter(value = "response", nulls = Nulls.SKIP) + public Builder response(Optional> response) { + this.response = response; + return this; + } + + public Builder response(List response) { + this.response = Optional.ofNullable(response); + return this; + } + + public V1FlowDetokenizeResponse build() { + return new V1FlowDetokenizeResponse(response, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java new file mode 100644 index 00000000..f0cf909d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowDetokenizeResponseObject.java @@ -0,0 +1,249 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowDetokenizeResponseObject.Builder.class) +public final class V1FlowDetokenizeResponseObject { + private final Optional token; + + private final Optional value; + + private final Optional tokenGroupName; + + private final Optional error; + + private final Optional httpCode; + + private final Optional> metadata; + + private final Map additionalProperties; + + private V1FlowDetokenizeResponseObject( + Optional token, + Optional value, + Optional tokenGroupName, + Optional error, + Optional httpCode, + Optional> metadata, + Map additionalProperties) { + this.token = token; + this.value = value; + this.tokenGroupName = tokenGroupName; + this.error = error; + this.httpCode = httpCode; + this.metadata = metadata; + this.additionalProperties = additionalProperties; + } + + /** + * @return Token to be detokenized + */ + @JsonProperty("token") + public Optional getToken() { + return token; + } + + /** + * @return Detokenized value for the token + */ + @JsonProperty("value") + public Optional getValue() { + return value; + } + + /** + * @return Token group name + */ + @JsonProperty("tokenGroupName") + public Optional getTokenGroupName() { + return tokenGroupName; + } + + /** + * @return Error if detokenization failed + */ + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; + } + + /** + * @return Additional metadata associated with the token, such as tableName or skyflowID + */ + @JsonProperty("metadata") + public Optional> getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowDetokenizeResponseObject && equalTo((V1FlowDetokenizeResponseObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowDetokenizeResponseObject other) { + return token.equals(other.token) + && value.equals(other.value) + && tokenGroupName.equals(other.tokenGroupName) + && error.equals(other.error) + && httpCode.equals(other.httpCode) + && metadata.equals(other.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(this.token, this.value, this.tokenGroupName, this.error, this.httpCode, this.metadata); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional token = Optional.empty(); + + private Optional value = Optional.empty(); + + private Optional tokenGroupName = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); + + private Optional> metadata = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowDetokenizeResponseObject other) { + token(other.getToken()); + value(other.getValue()); + tokenGroupName(other.getTokenGroupName()); + error(other.getError()); + httpCode(other.getHttpCode()); + metadata(other.getMetadata()); + return this; + } + + /** + *

Token to be detokenized

+ */ + @JsonSetter(value = "token", nulls = Nulls.SKIP) + public Builder token(Optional token) { + this.token = token; + return this; + } + + public Builder token(String token) { + this.token = Optional.ofNullable(token); + return this; + } + + /** + *

Detokenized value for the token

+ */ + @JsonSetter(value = "value", nulls = Nulls.SKIP) + public Builder value(Optional value) { + this.value = value; + return this; + } + + public Builder value(Object value) { + this.value = Optional.ofNullable(value); + return this; + } + + /** + *

Token group name

+ */ + @JsonSetter(value = "tokenGroupName", nulls = Nulls.SKIP) + public Builder tokenGroupName(Optional tokenGroupName) { + this.tokenGroupName = tokenGroupName; + return this; + } + + public Builder tokenGroupName(String tokenGroupName) { + this.tokenGroupName = Optional.ofNullable(tokenGroupName); + return this; + } + + /** + *

Error if detokenization failed

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

+ */ + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; + return this; + } + + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); + return this; + } + + /** + *

Additional metadata associated with the token, such as tableName or skyflowID

+ */ + @JsonSetter(value = "metadata", nulls = Nulls.SKIP) + public Builder metadata(Optional> metadata) { + this.metadata = metadata; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = Optional.ofNullable(metadata); + return this; + } + + public V1FlowDetokenizeResponseObject build() { + return new V1FlowDetokenizeResponseObject( + token, value, tokenGroupName, error, httpCode, metadata, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java new file mode 100644 index 00000000..adefcaed --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeRequestObject.java @@ -0,0 +1,153 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowTokenizeRequestObject.Builder.class) +public final class V1FlowTokenizeRequestObject { + private final Optional value; + + private final Optional> tokenGroupNames; + + private final Optional token; + + private final Map additionalProperties; + + private V1FlowTokenizeRequestObject( + Optional value, + Optional> tokenGroupNames, + Optional token, + Map additionalProperties) { + this.value = value; + this.tokenGroupNames = tokenGroupNames; + this.token = token; + this.additionalProperties = additionalProperties; + } + + /** + * @return Token Value + */ + @JsonProperty("value") + public Optional getValue() { + return value; + } + + /** + * @return List of token group names + */ + @JsonProperty("tokenGroupNames") + public Optional> getTokenGroupNames() { + return tokenGroupNames; + } + + /** + * @return Token for the value, in case of BYOT. + */ + @JsonProperty("token") + public Optional getToken() { + return token; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowTokenizeRequestObject && equalTo((V1FlowTokenizeRequestObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowTokenizeRequestObject other) { + return value.equals(other.value) && tokenGroupNames.equals(other.tokenGroupNames) && token.equals(other.token); + } + + @Override + public int hashCode() { + return Objects.hash(this.value, this.tokenGroupNames, this.token); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional value = Optional.empty(); + + private Optional> tokenGroupNames = Optional.empty(); + + private Optional token = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowTokenizeRequestObject other) { + value(other.getValue()); + tokenGroupNames(other.getTokenGroupNames()); + token(other.getToken()); + return this; + } + + /** + *

Token Value

+ */ + @JsonSetter(value = "value", nulls = Nulls.SKIP) + public Builder value(Optional value) { + this.value = value; + return this; + } + + public Builder value(Object value) { + this.value = Optional.ofNullable(value); + return this; + } + + /** + *

List of token group names

+ */ + @JsonSetter(value = "tokenGroupNames", nulls = Nulls.SKIP) + public Builder tokenGroupNames(Optional> tokenGroupNames) { + this.tokenGroupNames = tokenGroupNames; + return this; + } + + public Builder tokenGroupNames(List tokenGroupNames) { + this.tokenGroupNames = Optional.ofNullable(tokenGroupNames); + return this; + } + + /** + *

Token for the value, in case of BYOT.

+ */ + @JsonSetter(value = "token", nulls = Nulls.SKIP) + public Builder token(Optional token) { + this.token = token; + return this; + } + + public Builder token(Object token) { + this.token = Optional.ofNullable(token); + return this; + } + + public V1FlowTokenizeRequestObject build() { + return new V1FlowTokenizeRequestObject(value, tokenGroupNames, token, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java new file mode 100644 index 00000000..cf071b86 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponse.java @@ -0,0 +1,94 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowTokenizeResponse.Builder.class) +public final class V1FlowTokenizeResponse { + private final Optional> response; + + private final Map additionalProperties; + + private V1FlowTokenizeResponse( + Optional> response, Map additionalProperties) { + this.response = response; + this.additionalProperties = additionalProperties; + } + + /** + * @return Tokenized data + */ + @JsonProperty("response") + public Optional> getResponse() { + return response; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowTokenizeResponse && equalTo((V1FlowTokenizeResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowTokenizeResponse other) { + return response.equals(other.response); + } + + @Override + public int hashCode() { + return Objects.hash(this.response); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> response = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowTokenizeResponse other) { + response(other.getResponse()); + return this; + } + + /** + *

Tokenized data

+ */ + @JsonSetter(value = "response", nulls = Nulls.SKIP) + public Builder response(Optional> response) { + this.response = response; + return this; + } + + public Builder response(List response) { + this.response = Optional.ofNullable(response); + return this; + } + + public V1FlowTokenizeResponse build() { + return new V1FlowTokenizeResponse(response, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java new file mode 100644 index 00000000..eb4a15ac --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowTokenizeResponseObject.java @@ -0,0 +1,124 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowTokenizeResponseObject.Builder.class) +public final class V1FlowTokenizeResponseObject { + private final Optional value; + + private final Optional> tokens; + + private final Map additionalProperties; + + private V1FlowTokenizeResponseObject( + Optional value, + Optional> tokens, + Map additionalProperties) { + this.value = value; + this.tokens = tokens; + this.additionalProperties = additionalProperties; + } + + /** + * @return Value of token + */ + @JsonProperty("value") + public Optional getValue() { + return value; + } + + /** + * @return Token value + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowTokenizeResponseObject && equalTo((V1FlowTokenizeResponseObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowTokenizeResponseObject other) { + return value.equals(other.value) && tokens.equals(other.tokens); + } + + @Override + public int hashCode() { + return Objects.hash(this.value, this.tokens); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional value = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowTokenizeResponseObject other) { + value(other.getValue()); + tokens(other.getTokens()); + return this; + } + + /** + *

Value of token

+ */ + @JsonSetter(value = "value", nulls = Nulls.SKIP) + public Builder value(Optional value) { + this.value = value; + return this; + } + + public Builder value(Object value) { + this.value = Optional.ofNullable(value); + return this; + } + + /** + *

Token value

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(List tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + public V1FlowTokenizeResponseObject build() { + return new V1FlowTokenizeResponseObject(value, tokens, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java new file mode 100644 index 00000000..5345de38 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsData.java @@ -0,0 +1,96 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowVaultMetricsData.Builder.class) +public final class V1FlowVaultMetricsData { + private final Optional> tables; + + private final Map additionalProperties; + + private V1FlowVaultMetricsData(Optional> tables, Map additionalProperties) { + this.tables = tables; + this.additionalProperties = additionalProperties; + } + + /** + * @return Map of table names to their metrics + */ + @JsonProperty("tables") + public Optional> getTables() { + return tables; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowVaultMetricsData && equalTo((V1FlowVaultMetricsData) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowVaultMetricsData other) { + return tables.equals(other.tables); + } + + @Override + public int hashCode() { + return Objects.hash(this.tables); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> tables = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowVaultMetricsData other) { + tables(other.getTables()); + return this; + } + + /** + *

Map of table names to their metrics

+ */ + @JsonSetter(value = "tables", nulls = Nulls.SKIP) + public Builder tables(Optional> tables) { + this.tables = tables; + return this; + } + + public Builder tables(Map tables) { + this.tables = Optional.ofNullable(tables); + return this; + } + + public V1FlowVaultMetricsData build() { + return new V1FlowVaultMetricsData(tables, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java new file mode 100644 index 00000000..f933bce5 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1FlowVaultMetricsResponse.java @@ -0,0 +1,121 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1FlowVaultMetricsResponse.Builder.class) +public final class V1FlowVaultMetricsResponse { + private final Optional data; + + private final Optional> error; + + private final Map additionalProperties; + + private V1FlowVaultMetricsResponse( + Optional data, + Optional> error, + Map additionalProperties) { + this.data = data; + this.error = error; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("data") + public Optional getData() { + return data; + } + + /** + * @return Error information, if any + */ + @JsonProperty("error") + public Optional> getError() { + return error; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1FlowVaultMetricsResponse && equalTo((V1FlowVaultMetricsResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1FlowVaultMetricsResponse other) { + return data.equals(other.data) && error.equals(other.error); + } + + @Override + public int hashCode() { + return Objects.hash(this.data, this.error); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional data = Optional.empty(); + + private Optional> error = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1FlowVaultMetricsResponse other) { + data(other.getData()); + error(other.getError()); + return this; + } + + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional data) { + this.data = data; + return this; + } + + public Builder data(V1FlowVaultMetricsData data) { + this.data = Optional.ofNullable(data); + return this; + } + + /** + *

Error information, if any

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional> error) { + this.error = error; + return this; + } + + public Builder error(Map error) { + this.error = Optional.ofNullable(error); + return this; + } + + public V1FlowVaultMetricsResponse build() { + return new V1FlowVaultMetricsResponse(data, error, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java new file mode 100644 index 00000000..0c0cfab9 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetRequestData.java @@ -0,0 +1,216 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1GetRequestData.Builder.class) +public final class V1GetRequestData { + private final Optional tableName; + + private final Optional> skyflowIDs; + + private final Optional> columnRedactions; + + private final Optional> columns; + + private final Optional> uniqueValues; + + private final Map additionalProperties; + + private V1GetRequestData( + Optional tableName, + Optional> skyflowIDs, + Optional> columnRedactions, + Optional> columns, + Optional> uniqueValues, + Map additionalProperties) { + this.tableName = tableName; + this.skyflowIDs = skyflowIDs; + this.columnRedactions = columnRedactions; + this.columns = columns; + this.uniqueValues = uniqueValues; + this.additionalProperties = additionalProperties; + } + + /** + * @return Name of the table where data is being fetched + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + /** + * @return Skyflow ID for the record to be fetched + */ + @JsonProperty("skyflowIDs") + public Optional> getSkyflowIDs() { + return skyflowIDs; + } + + /** + * @return List of columns to be redacted. + */ + @JsonProperty("columnRedactions") + public Optional> getColumnRedactions() { + return columnRedactions; + } + + /** + * @return List of columns to be fetched. + */ + @JsonProperty("columns") + public Optional> getColumns() { + return columns; + } + + /** + * @return List of unique constraint values to query records by data + */ + @JsonProperty("uniqueValues") + public Optional> getUniqueValues() { + return uniqueValues; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1GetRequestData && equalTo((V1GetRequestData) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1GetRequestData other) { + return tableName.equals(other.tableName) + && skyflowIDs.equals(other.skyflowIDs) + && columnRedactions.equals(other.columnRedactions) + && columns.equals(other.columns) + && uniqueValues.equals(other.uniqueValues); + } + + @Override + public int hashCode() { + return Objects.hash(this.tableName, this.skyflowIDs, this.columnRedactions, this.columns, this.uniqueValues); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional tableName = Optional.empty(); + + private Optional> skyflowIDs = Optional.empty(); + + private Optional> columnRedactions = Optional.empty(); + + private Optional> columns = Optional.empty(); + + private Optional> uniqueValues = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1GetRequestData other) { + tableName(other.getTableName()); + skyflowIDs(other.getSkyflowIDs()); + columnRedactions(other.getColumnRedactions()); + columns(other.getColumns()); + uniqueValues(other.getUniqueValues()); + return this; + } + + /** + *

Name of the table where data is being fetched

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + /** + *

Skyflow ID for the record to be fetched

+ */ + @JsonSetter(value = "skyflowIDs", nulls = Nulls.SKIP) + public Builder skyflowIDs(Optional> skyflowIDs) { + this.skyflowIDs = skyflowIDs; + return this; + } + + public Builder skyflowIDs(List skyflowIDs) { + this.skyflowIDs = Optional.ofNullable(skyflowIDs); + return this; + } + + /** + *

List of columns to be redacted.

+ */ + @JsonSetter(value = "columnRedactions", nulls = Nulls.SKIP) + public Builder columnRedactions(Optional> columnRedactions) { + this.columnRedactions = columnRedactions; + return this; + } + + public Builder columnRedactions(List columnRedactions) { + this.columnRedactions = Optional.ofNullable(columnRedactions); + return this; + } + + /** + *

List of columns to be fetched.

+ */ + @JsonSetter(value = "columns", nulls = Nulls.SKIP) + public Builder columns(Optional> columns) { + this.columns = columns; + return this; + } + + public Builder columns(List columns) { + this.columns = Optional.ofNullable(columns); + return this; + } + + /** + *

List of unique constraint values to query records by data

+ */ + @JsonSetter(value = "uniqueValues", nulls = Nulls.SKIP) + public Builder uniqueValues(Optional> uniqueValues) { + this.uniqueValues = uniqueValues; + return this; + } + + public Builder uniqueValues(List uniqueValues) { + this.uniqueValues = Optional.ofNullable(uniqueValues); + return this; + } + + public V1GetRequestData build() { + return new V1GetRequestData( + tableName, skyflowIDs, columnRedactions, columns, uniqueValues, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java new file mode 100644 index 00000000..a64a7588 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1GetResponse.java @@ -0,0 +1,93 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1GetResponse.Builder.class) +public final class V1GetResponse { + private final Optional> records; + + private final Map additionalProperties; + + private V1GetResponse(Optional> records, Map additionalProperties) { + this.records = records; + this.additionalProperties = additionalProperties; + } + + /** + * @return List of fetched records with skyflow ID, tokens, data, and any partial errors + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1GetResponse && equalTo((V1GetResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1GetResponse other) { + return records.equals(other.records); + } + + @Override + public int hashCode() { + return Objects.hash(this.records); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> records = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1GetResponse other) { + records(other.getRecords()); + return this; + } + + /** + *

List of fetched records with skyflow ID, tokens, data, and any partial errors

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + public V1GetResponse build() { + return new V1GetResponse(records, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java new file mode 100644 index 00000000..7de162cc --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordData.java @@ -0,0 +1,182 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1InsertRecordData.Builder.class) +public final class V1InsertRecordData { + private final Optional> data; + + private final Optional> tokens; + + private final Optional tableName; + + private final Optional upsert; + + private final Map additionalProperties; + + private V1InsertRecordData( + Optional> data, + Optional> tokens, + Optional tableName, + Optional upsert, + Map additionalProperties) { + this.data = data; + this.tokens = tokens; + this.tableName = tableName; + this.upsert = upsert; + this.additionalProperties = additionalProperties; + } + + /** + * @return Columns names and values + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + /** + * @return undocumented_field; Tokens data for the columns if any + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + /** + * @return Table name for the record + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + @JsonProperty("upsert") + public Optional getUpsert() { + return upsert; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1InsertRecordData && equalTo((V1InsertRecordData) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1InsertRecordData other) { + return data.equals(other.data) + && tokens.equals(other.tokens) + && tableName.equals(other.tableName) + && upsert.equals(other.upsert); + } + + @Override + public int hashCode() { + return Objects.hash(this.data, this.tokens, this.tableName, this.upsert); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> data = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + private Optional tableName = Optional.empty(); + + private Optional upsert = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1InsertRecordData other) { + data(other.getData()); + tokens(other.getTokens()); + tableName(other.getTableName()); + upsert(other.getUpsert()); + return this; + } + + /** + *

Columns names and values

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(Map data) { + this.data = Optional.ofNullable(data); + return this; + } + + /** + *

undocumented_field; Tokens data for the columns if any

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(Map tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + /** + *

Table name for the record

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + @JsonSetter(value = "upsert", nulls = Nulls.SKIP) + public Builder upsert(Optional upsert) { + this.upsert = upsert; + return this; + } + + public Builder upsert(V1Upsert upsert) { + this.upsert = Optional.ofNullable(upsert); + return this; + } + + public V1InsertRecordData build() { + return new V1InsertRecordData(data, tokens, tableName, upsert, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java new file mode 100644 index 00000000..4ff3ff2d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1InsertResponse.java @@ -0,0 +1,93 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1InsertResponse.Builder.class) +public final class V1InsertResponse { + private final Optional> records; + + private final Map additionalProperties; + + private V1InsertResponse(Optional> records, Map additionalProperties) { + this.records = records; + this.additionalProperties = additionalProperties; + } + + /** + * @return List of inserted records with skyflow ID, tokens, data, and any partial errors. + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1InsertResponse && equalTo((V1InsertResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1InsertResponse other) { + return records.equals(other.records); + } + + @Override + public int hashCode() { + return Objects.hash(this.records); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> records = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1InsertResponse other) { + records(other.getRecords()); + return this; + } + + /** + *

List of inserted records with skyflow ID, tokens, data, and any partial errors.

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + public V1InsertResponse build() { + return new V1InsertResponse(records, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java new file mode 100644 index 00000000..b6c2467c --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1RecordResponseObject.java @@ -0,0 +1,280 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1RecordResponseObject.Builder.class) +public final class V1RecordResponseObject { + private final Optional skyflowId; + + private final Optional> tokens; + + private final Optional> data; + + private final Optional> hashedData; + + private final Optional error; + + private final Optional httpCode; + + private final Optional tableName; + + private final Map additionalProperties; + + private V1RecordResponseObject( + Optional skyflowId, + Optional> tokens, + Optional> data, + Optional> hashedData, + Optional error, + Optional httpCode, + Optional tableName, + Map additionalProperties) { + this.skyflowId = skyflowId; + this.tokens = tokens; + this.data = data; + this.hashedData = hashedData; + this.error = error; + this.httpCode = httpCode; + this.tableName = tableName; + this.additionalProperties = additionalProperties; + } + + /** + * @return Skyflow ID for the inserted record + */ + @JsonProperty("skyflowID") + public Optional getSkyflowId() { + return skyflowId; + } + + /** + * @return Tokens data for the columns if any + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + /** + * @return Columns names and values + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + /** + * @return Hashed Data for the columns if any + */ + @JsonProperty("hashedData") + public Optional> getHashedData() { + return hashedData; + } + + /** + * @return Partial Error message if any + */ + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return HTTP status code of the response + */ + @JsonProperty("httpCode") + public Optional getHttpCode() { + return httpCode; + } + + /** + * @return Name of the table record belongs to + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1RecordResponseObject && equalTo((V1RecordResponseObject) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1RecordResponseObject other) { + return skyflowId.equals(other.skyflowId) + && tokens.equals(other.tokens) + && data.equals(other.data) + && hashedData.equals(other.hashedData) + && error.equals(other.error) + && httpCode.equals(other.httpCode) + && tableName.equals(other.tableName); + } + + @Override + public int hashCode() { + return Objects.hash( + this.skyflowId, this.tokens, this.data, this.hashedData, this.error, this.httpCode, this.tableName); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional skyflowId = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + private Optional> data = Optional.empty(); + + private Optional> hashedData = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional httpCode = Optional.empty(); + + private Optional tableName = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1RecordResponseObject other) { + skyflowId(other.getSkyflowId()); + tokens(other.getTokens()); + data(other.getData()); + hashedData(other.getHashedData()); + error(other.getError()); + httpCode(other.getHttpCode()); + tableName(other.getTableName()); + return this; + } + + /** + *

Skyflow ID for the inserted record

+ */ + @JsonSetter(value = "skyflowID", nulls = Nulls.SKIP) + public Builder skyflowId(Optional skyflowId) { + this.skyflowId = skyflowId; + return this; + } + + public Builder skyflowId(String skyflowId) { + this.skyflowId = Optional.ofNullable(skyflowId); + return this; + } + + /** + *

Tokens data for the columns if any

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(Map tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + /** + *

Columns names and values

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(Map data) { + this.data = Optional.ofNullable(data); + return this; + } + + /** + *

Hashed Data for the columns if any

+ */ + @JsonSetter(value = "hashedData", nulls = Nulls.SKIP) + public Builder hashedData(Optional> hashedData) { + this.hashedData = hashedData; + return this; + } + + public Builder hashedData(Map hashedData) { + this.hashedData = Optional.ofNullable(hashedData); + return this; + } + + /** + *

Partial Error message if any

+ */ + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public Builder error(Optional error) { + this.error = error; + return this; + } + + public Builder error(String error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

HTTP status code of the response

+ */ + @JsonSetter(value = "httpCode", nulls = Nulls.SKIP) + public Builder httpCode(Optional httpCode) { + this.httpCode = httpCode; + return this; + } + + public Builder httpCode(Integer httpCode) { + this.httpCode = Optional.ofNullable(httpCode); + return this; + } + + /** + *

Name of the table record belongs to

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + public V1RecordResponseObject build() { + return new V1RecordResponseObject( + skyflowId, tokens, data, hashedData, error, httpCode, tableName, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java new file mode 100644 index 00000000..5d5fc97f --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1TokenGroupRedactions.java @@ -0,0 +1,125 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1TokenGroupRedactions.Builder.class) +public final class V1TokenGroupRedactions { + private final Optional tokenGroupName; + + private final Optional redaction; + + private final Map additionalProperties; + + private V1TokenGroupRedactions( + Optional tokenGroupName, Optional redaction, Map additionalProperties) { + this.tokenGroupName = tokenGroupName; + this.redaction = redaction; + this.additionalProperties = additionalProperties; + } + + /** + * @return Name of the token group to be redacted + */ + @JsonProperty("tokenGroupName") + public Optional getTokenGroupName() { + return tokenGroupName; + } + + /** + * @return Name of the redaction. Eg: plain_text, redacted, mask1 + */ + @JsonProperty("redaction") + public Optional getRedaction() { + return redaction; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1TokenGroupRedactions && equalTo((V1TokenGroupRedactions) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1TokenGroupRedactions other) { + return tokenGroupName.equals(other.tokenGroupName) && redaction.equals(other.redaction); + } + + @Override + public int hashCode() { + return Objects.hash(this.tokenGroupName, this.redaction); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional tokenGroupName = Optional.empty(); + + private Optional redaction = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1TokenGroupRedactions other) { + tokenGroupName(other.getTokenGroupName()); + redaction(other.getRedaction()); + return this; + } + + /** + *

Name of the token group to be redacted

+ */ + @JsonSetter(value = "tokenGroupName", nulls = Nulls.SKIP) + public Builder tokenGroupName(Optional tokenGroupName) { + this.tokenGroupName = tokenGroupName; + return this; + } + + public Builder tokenGroupName(String tokenGroupName) { + this.tokenGroupName = Optional.ofNullable(tokenGroupName); + return this; + } + + /** + *

Name of the redaction. Eg: plain_text, redacted, mask1

+ */ + @JsonSetter(value = "redaction", nulls = Nulls.SKIP) + public Builder redaction(Optional redaction) { + this.redaction = redaction; + return this; + } + + public Builder redaction(String redaction) { + this.redaction = Optional.ofNullable(redaction); + return this; + } + + public V1TokenGroupRedactions build() { + return new V1TokenGroupRedactions(tokenGroupName, redaction, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java new file mode 100644 index 00000000..3b188c6d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UniqueValue.java @@ -0,0 +1,96 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1UniqueValue.Builder.class) +public final class V1UniqueValue { + private final Optional> data; + + private final Map additionalProperties; + + private V1UniqueValue(Optional> data, Map additionalProperties) { + this.data = data; + this.additionalProperties = additionalProperties; + } + + /** + * @return Columns names and values for unique value entry + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1UniqueValue && equalTo((V1UniqueValue) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1UniqueValue other) { + return data.equals(other.data); + } + + @Override + public int hashCode() { + return Objects.hash(this.data); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> data = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1UniqueValue other) { + data(other.getData()); + return this; + } + + /** + *

Columns names and values for unique value entry

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(Map data) { + this.data = Optional.ofNullable(data); + return this; + } + + public V1UniqueValue build() { + return new V1UniqueValue(data, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java new file mode 100644 index 00000000..5a904005 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordData.java @@ -0,0 +1,188 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1UpdateRecordData.Builder.class) +public final class V1UpdateRecordData { + private final Optional skyflowId; + + private final Optional> data; + + private final Optional> tokens; + + private final Optional tableName; + + private final Map additionalProperties; + + private V1UpdateRecordData( + Optional skyflowId, + Optional> data, + Optional> tokens, + Optional tableName, + Map additionalProperties) { + this.skyflowId = skyflowId; + this.data = data; + this.tokens = tokens; + this.tableName = tableName; + this.additionalProperties = additionalProperties; + } + + /** + * @return Skyflow ID for the record to be updated + */ + @JsonProperty("skyflowID") + public Optional getSkyflowId() { + return skyflowId; + } + + /** + * @return List of data row wise that is to be updated in the vault + */ + @JsonProperty("data") + public Optional> getData() { + return data; + } + + /** + * @return undocumented_field; Tokens data for the columns if any + */ + @JsonProperty("tokens") + public Optional> getTokens() { + return tokens; + } + + /** + * @return Table name for the record + */ + @JsonProperty("tableName") + public Optional getTableName() { + return tableName; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1UpdateRecordData && equalTo((V1UpdateRecordData) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1UpdateRecordData other) { + return skyflowId.equals(other.skyflowId) + && data.equals(other.data) + && tokens.equals(other.tokens) + && tableName.equals(other.tableName); + } + + @Override + public int hashCode() { + return Objects.hash(this.skyflowId, this.data, this.tokens, this.tableName); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional skyflowId = Optional.empty(); + + private Optional> data = Optional.empty(); + + private Optional> tokens = Optional.empty(); + + private Optional tableName = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1UpdateRecordData other) { + skyflowId(other.getSkyflowId()); + data(other.getData()); + tokens(other.getTokens()); + tableName(other.getTableName()); + return this; + } + + /** + *

Skyflow ID for the record to be updated

+ */ + @JsonSetter(value = "skyflowID", nulls = Nulls.SKIP) + public Builder skyflowId(Optional skyflowId) { + this.skyflowId = skyflowId; + return this; + } + + public Builder skyflowId(String skyflowId) { + this.skyflowId = Optional.ofNullable(skyflowId); + return this; + } + + /** + *

List of data row wise that is to be updated in the vault

+ */ + @JsonSetter(value = "data", nulls = Nulls.SKIP) + public Builder data(Optional> data) { + this.data = data; + return this; + } + + public Builder data(Map data) { + this.data = Optional.ofNullable(data); + return this; + } + + /** + *

undocumented_field; Tokens data for the columns if any

+ */ + @JsonSetter(value = "tokens", nulls = Nulls.SKIP) + public Builder tokens(Optional> tokens) { + this.tokens = tokens; + return this; + } + + public Builder tokens(Map tokens) { + this.tokens = Optional.ofNullable(tokens); + return this; + } + + /** + *

Table name for the record

+ */ + @JsonSetter(value = "tableName", nulls = Nulls.SKIP) + public Builder tableName(Optional tableName) { + this.tableName = tableName; + return this; + } + + public Builder tableName(String tableName) { + this.tableName = Optional.ofNullable(tableName); + return this; + } + + public V1UpdateRecordData build() { + return new V1UpdateRecordData(skyflowId, data, tokens, tableName, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java new file mode 100644 index 00000000..38dfe0ab --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateResponse.java @@ -0,0 +1,93 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1UpdateResponse.Builder.class) +public final class V1UpdateResponse { + private final Optional> records; + + private final Map additionalProperties; + + private V1UpdateResponse(Optional> records, Map additionalProperties) { + this.records = records; + this.additionalProperties = additionalProperties; + } + + /** + * @return List of updated records with skyflow ID, tokens, data, and any partial errors + */ + @JsonProperty("records") + public Optional> getRecords() { + return records; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1UpdateResponse && equalTo((V1UpdateResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1UpdateResponse other) { + return records.equals(other.records); + } + + @Override + public int hashCode() { + return Objects.hash(this.records); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> records = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1UpdateResponse other) { + records(other.getRecords()); + return this; + } + + /** + *

List of updated records with skyflow ID, tokens, data, and any partial errors

+ */ + @JsonSetter(value = "records", nulls = Nulls.SKIP) + public Builder records(Optional> records) { + this.records = records; + return this; + } + + public Builder records(List records) { + this.records = Optional.ofNullable(records); + return this; + } + + public V1UpdateResponse build() { + return new V1UpdateResponse(records, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java new file mode 100644 index 00000000..a0c27b0e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/generated/rest/types/V1Upsert.java @@ -0,0 +1,118 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.types; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skyflow.generated.rest.core.ObjectMappers; + +import java.util.*; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = V1Upsert.Builder.class) +public final class V1Upsert { + private final Optional updateType; + + private final Optional> uniqueColumns; + + private final Map additionalProperties; + + private V1Upsert( + Optional updateType, + Optional> uniqueColumns, + Map additionalProperties) { + this.updateType = updateType; + this.uniqueColumns = uniqueColumns; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("updateType") + public Optional getUpdateType() { + return updateType; + } + + /** + * @return Name of a unique columns in the table. Uses upsert operations to check if a record exists based on the unique column's value. If a matching record exists, the record updates with the values you provide. If a matching record doesn't exist, the upsert operation inserts a new record. + */ + @JsonProperty("uniqueColumns") + public Optional> getUniqueColumns() { + return uniqueColumns; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof V1Upsert && equalTo((V1Upsert) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(V1Upsert other) { + return updateType.equals(other.updateType) && uniqueColumns.equals(other.uniqueColumns); + } + + @Override + public int hashCode() { + return Objects.hash(this.updateType, this.uniqueColumns); + } + + @Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional updateType = Optional.empty(); + + private Optional> uniqueColumns = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(V1Upsert other) { + updateType(other.getUpdateType()); + uniqueColumns(other.getUniqueColumns()); + return this; + } + + @JsonSetter(value = "updateType", nulls = Nulls.SKIP) + public Builder updateType(Optional updateType) { + this.updateType = updateType; + return this; + } + + public Builder updateType(FlowEnumUpdateType updateType) { + this.updateType = Optional.ofNullable(updateType); + return this; + } + + /** + *

Name of a unique columns in the table. Uses upsert operations to check if a record exists based on the unique column's value. If a matching record exists, the record updates with the values you provide. If a matching record doesn't exist, the upsert operation inserts a new record.

+ */ + @JsonSetter(value = "uniqueColumns", nulls = Nulls.SKIP) + public Builder uniqueColumns(Optional> uniqueColumns) { + this.uniqueColumns = uniqueColumns; + return this; + } + + public Builder uniqueColumns(List uniqueColumns) { + this.uniqueColumns = Optional.ofNullable(uniqueColumns); + return this; + } + + public V1Upsert build() { + return new V1Upsert(updateType, uniqueColumns, additionalProperties); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/utils/Constants.java b/flowvault/src/main/java/com/skyflow/utils/Constants.java new file mode 100644 index 00000000..acbd471f --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/utils/Constants.java @@ -0,0 +1,51 @@ +package com.skyflow.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public final class Constants extends BaseConstants { + public static final String SDK_NAME = "Skyflow Java SDK"; + public static final String SDK_VERSION; + public static final String VAULT_DOMAIN = ".skyvault."; + public static final String SDK_PREFIX; + public static final String SDK_METRIC_NAME_VERSION_PREFIX = "skyflow-flowvault-java@"; + public static final Integer MAX_BULK_DATA_SIZE = 10000; + public static final Integer INSERT_BATCH_SIZE = 50; + public static final Integer MAX_INSERT_BATCH_SIZE = 1000; + public static final Integer INSERT_CONCURRENCY_LIMIT = 1; + public static final Integer MAX_INSERT_CONCURRENCY_LIMIT = 10; + public static final Integer DETOKENIZE_BATCH_SIZE = 50; + public static final Integer DETOKENIZE_CONCURRENCY_LIMIT = 1; + public static final Integer MAX_DETOKENIZE_BATCH_SIZE = 1000; + public static final Integer MAX_DETOKENIZE_CONCURRENCY_LIMIT = 10; + public static final Integer DELETE_TOKENS_BATCH_SIZE = 50; + public static final Integer DELETE_TOKENS_CONCURRENCY_LIMIT = 1; + public static final Integer MAX_DELETE_TOKENS_BATCH_SIZE = 1000; + public static final Integer MAX_DELETE_TOKENS_CONCURRENCY_LIMIT = 10; + public static final Integer TOKENIZE_BATCH_SIZE = 50; + public static final Integer TOKENIZE_CONCURRENCY_LIMIT = 1; + public static final Integer MAX_TOKENIZE_BATCH_SIZE = 1000; + public static final Integer MAX_TOKENIZE_CONCURRENCY_LIMIT = 10; + public static final String DEFAULT_SDK_VERSION = "v3"; + public static final String CONTEXT_KEY_REGEX = "^[a-zA-Z0-9_]+$"; + + static { + String sdkVersion; + // Use a static initializer block to read the properties file + Properties properties = new Properties(); + try (InputStream input = Constants.class.getClassLoader().getResourceAsStream("sdk.properties")) { + if (input == null) { + sdkVersion = DEFAULT_SDK_VERSION; + } else { + properties.load(input); + sdkVersion = properties.getProperty("sdk.version", DEFAULT_SDK_VERSION); + } + } catch (IOException ex) { + sdkVersion = DEFAULT_SDK_VERSION; + } + SDK_VERSION = sdkVersion; + SDK_PREFIX = SDK_NAME + " " + SDK_VERSION; + } + +} 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 new file mode 100644 index 00000000..d6011dec --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/utils/Utils.java @@ -0,0 +1,1032 @@ +package com.skyflow.utils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +import com.google.gson.JsonObject; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ObjectMappers; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1InsertResponse; +import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; +import com.skyflow.generated.rest.types.V1Upsert; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.utils.logger.LogUtil; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeResponseRecord; +import com.skyflow.vault.data.DeleteTokensRecord; +import com.skyflow.vault.data.TokenizeRequestRecord; +import com.skyflow.vault.data.TokenizeResponseToken; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeResponse; +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.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. + return BaseUtils.getVaultURL(clusterId, env, Constants.VAULT_DOMAIN); + } + + public static JsonObject getMetrics() { + JsonObject details = getCommonMetrics(); + String sdkVersion = Constants.SDK_VERSION; + details.addProperty(Constants.SDK_METRIC_NAME_VERSION, Constants.SDK_METRIC_NAME_VERSION_PREFIX + sdkVersion); + return details; + } + + public static String getEnvVaultUrl() throws SkyflowException { + try { + String vaultUrl = System.getenv("VAULT_URL"); + if (vaultUrl == null) { + Dotenv dotenv = Dotenv.load(); + vaultUrl = dotenv.get("VAULT_URL"); + } + if (vaultUrl != null && vaultUrl.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage()); + } else if (vaultUrl != null && !isValidUrl(vaultUrl)) { + LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); + } + return vaultUrl; + } catch (DotenvException e) { + return null; + } + } + + public static boolean isValidUrl(String url) { + URL parsedUrl; + try { + parsedUrl = new URL(url); + } catch (MalformedURLException e) { + return false; + } + + if (!parsedUrl.getProtocol().equalsIgnoreCase("https")) { + return false; + } else { + return parsedUrl.getHost() != null && !parsedUrl.getHost().isEmpty(); + } + } + + // 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(); + } + + private static V1Upsert toV1Upsert(UpsertOptions upsert) { + V1Upsert.Builder builder = V1Upsert.builder().uniqueColumns(upsert.getUniqueColumns()); + // updateType is a String on the request; the legal values come from the wire enum itself + // so there is a single source of truth. Validations rejects anything that does not match. + String updateType = upsert.getUpdateType(); + for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { + if (type.toString().equalsIgnoreCase(updateType)) { + builder.updateType(type); + break; + } + } + return builder.build(); + } + + 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. + if (hasText(record.getTableName())) { + data.tableName(record.getTableName()); + } + if (record.getTokens() != null && !record.getTokens().isEmpty()) { + data.tokens(record.getTokens()); + } + UpsertOptions recordUpsert = record.getUpsert(); + if (recordUpsert != null && recordUpsert.getUniqueColumns() != null + && !recordUpsert.getUniqueColumns().isEmpty()) { + data.upsert(toV1Upsert(recordUpsert)); + } + insertRecordDataList.add(data.build()); + } + + V1InsertRequest.Builder builder = V1InsertRequest.builder() + .vaultId(config.getVaultId()) + .records(insertRecordDataList); + 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(); + } + + private static String extractRequestId(Map> headers) { + if (headers == null) return null; + List ids = headers.get(BaseConstants.REQUEST_ID_HEADER_KEY); + return (ids == null || ids.isEmpty()) ? null : ids.get(0); + } + + // ── Bulk (batched/concurrent) request-body builders ────────────────────── + + // BulkInsertRequest is an InsertRequest, so the bulk body is built exactly the same way. + public static com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest getBulkInsertRequestBody(BulkInsertRequest request, VaultConfig config) { + return getInsertRequestBody(request, config); + } + + public static V1FlowDetokenizeRequest getBulkDetokenizeRequestBody(BulkDetokenizeRequest request, String vaultId) { + V1FlowDetokenizeRequest.Builder builder = V1FlowDetokenizeRequest.builder() + .vaultId(vaultId) + .tokens(request.getTokens()); + if (request.getTokenGroupRedactions() != null && !request.getTokenGroupRedactions().isEmpty()) { + List tokenGroupRedactionsList = new ArrayList<>(); + for (TokenGroupRedactions tokenGroupRedactions : request.getTokenGroupRedactions()) { + tokenGroupRedactionsList.add(V1TokenGroupRedactions.builder() + .tokenGroupName(tokenGroupRedactions.getTokenGroupName()) + .redaction(tokenGroupRedactions.getRedaction()) + .build()); + } + builder.tokenGroupRedactions(tokenGroupRedactionsList); + } + return builder.build(); + } + + public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest getBulkDeleteTokensRequestBody(BulkDeleteTokensRequest request, String vaultId) { + return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .vaultId(vaultId) + .tokens(request.getTokens()) + .build(); + } + + public static com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest getBulkTokenizeRequestBody( + List records, String vaultId) { + List dataList = new ArrayList<>(); + for (BulkTokenizeRequestRecord record : records) { + dataList.add(buildTokenizeRequestObject(record)); + } + return com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId(vaultId) + .data(dataList) + .build(); + } + + // ── Bulk batching, exception-handling and response-formatting helpers ──── + + public static List> createBulkInsertBatches(List records, int batchSize) { + List> batches = new ArrayList<>(); + for (int i = 0; i < records.size(); i += batchSize) { + batches.add(records.subList(i, Math.min(i + batchSize, records.size()))); + } + return batches; + } + + public static List createBulkDetokenizeBatches(V1FlowDetokenizeRequest request, int batchSize) { + List detokenizeRequests = new ArrayList<>(); + List tokens = request.getTokens().get(); + + for (int i = 0; i < tokens.size(); i += batchSize) { + List batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size())); + List tokenGroupRedactions = null; + if (request.getTokenGroupRedactions().isPresent() && !request.getTokenGroupRedactions().get().isEmpty()) { + tokenGroupRedactions = request.getTokenGroupRedactions().get(); + } + V1FlowDetokenizeRequest batchRequest = V1FlowDetokenizeRequest.builder() + .vaultId(request.getVaultId()) + .tokens(new ArrayList<>(batchTokens)) + .tokenGroupRedactions(tokenGroupRedactions) + .build(); + + detokenizeRequests.add(batchRequest); + } + + return detokenizeRequests; + } + + public static List createBulkDeleteTokensBatches( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request, int batchSize) { + List batches = new ArrayList<>(); + List tokens = request.getTokens().get(); + for (int i = 0; i < tokens.size(); i += batchSize) { + List batchTokens = tokens.subList(i, Math.min(i + batchSize, tokens.size())); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batchRequest = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .vaultId(request.getVaultId()) + .tokens(new ArrayList<>(batchTokens)) + .build(); + batches.add(batchRequest); + } + return batches; + } + + /** + * 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 = 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; + } + + public static BulkDetokenizeResponseRecord createDetokenizeErrorRecord(Map recordMap, int indexNumber, String requestId) { + BulkDetokenizeResponseRecord err = null; + if (recordMap != null) { + int code = readHttpCode(recordMap, 500); + // the failing token is echoed back so the caller can tell which one it was + 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; + } + + public static ErrorRecord createErrorRecord(Map recordMap, int indexNumber, String requestId) { + ErrorRecord err = null; + if (recordMap != null) { + int code = readHttpCode(recordMap, 500); + String message = readErrorMessage(recordMap); + err = new ErrorRecord(indexNumber, message, code, requestId); + } + return err; + } + + // Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto + // the unified BulkInsertResponseRecord shape that bulk insert now returns. + public static List handleBulkInsertBatchException( + Throwable ex, List batch, int batchNumber, int batchSize + ) { + List allRecords = new ArrayList<>(); + 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 > 0 ? batchNumber * batchSize : 0; + if (responseBody != null) { + if (responseBody.containsKey("records")) { + Object recordss = responseBody.get("records"); + if (recordss instanceof List) { + List recordsList = (List) recordss; + for (Object record : recordsList) { + if (record instanceof Map) { + Map recordMap = (Map) record; + BulkInsertResponseRecord err = createInsertErrorRecord(recordMap, indexNumber, requestId); + allRecords.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; + for (int j = 0; j < batch.size(); j++) { + BulkInsertResponseRecord err = null; + if(recordMap != null){ + err = createInsertErrorRecord(recordMap, indexNumber, requestId); + } else { + String errorMessage = null; + if (fallbackMsg != null){ + errorMessage = fallbackMsg; + } else { + errorMessage = apiException.getMessage(); + } + err = new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + + } + allRecords.add(err); + indexNumber++; + } + } + } + + if (allRecords.isEmpty()) { + for (int j = 0; j < batch.size(); j++) { + allRecords.add(new BulkInsertResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); + indexNumber++; + } + } + } else { + int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; + for (int j = 0; j < batch.size(); j++) { + 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++; + } + } + return allRecords; + } + + // Errors are parsed into ErrorRecord (shared with the other bulk ops), then projected onto + // the unified BulkDetokenizeResponseRecord shape that bulk detokenize now returns. + public static List handleBulkDetokenizeBatchException( + Throwable ex, V1FlowDetokenizeRequest batch, int batchNumber, int batchSize + ) { + List allRecords = new ArrayList<>(); + 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; + int tokenCount = batch.getTokens().isPresent() ? batch.getTokens().get().size() : 0; + if (responseBody != null) { + if (responseBody.containsKey("response")) { + Object recordss = responseBody.get("response"); + if (recordss instanceof List) { + List recordsList = (List) recordss; + for (Object record : recordsList) { + if (record instanceof Map) { + Map recordMap = (Map) record; + BulkDetokenizeResponseRecord err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId); + allRecords.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; + for (int j = 0; j < tokenCount; j++) { + BulkDetokenizeResponseRecord err = null; + if (recordMap != null) { + err = createDetokenizeErrorRecord(recordMap, indexNumber, requestId); + } else { + String errorMessage = null; + if (fallbackMsg != null) { + errorMessage = fallbackMsg; + } else { + errorMessage = apiException.getMessage(); + } + err = new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), errorMessage, requestId); + } + allRecords.add(err); + indexNumber++; + } + } + } + + if (allRecords.isEmpty()) { + for (int j = 0; j < tokenCount; j++) { + allRecords.add(new BulkDetokenizeResponseRecord(indexNumber, null, null, null, null, apiException.statusCode(), apiException.getMessage(), requestId)); + indexNumber++; + } + } + } 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, message, null); + allRecords.add(err); + indexNumber++; + } + } + return allRecords; + } + + /** + * Best available description of a failure that never reached the API. + * + *

The generated client wraps transport failures as "Network error executing HTTP request", + * which says nothing about what actually went wrong, and the future wraps that again. Walk down + * to the innermost cause so the caller sees the real problem - for a mistyped cluster id that is + * {@code java.net.UnknownHostException: : nodename nor servname provided, or not known} + * rather than the generic wrapper. Mirrors the order bulk insert and detokenize already use. + */ + private static String describeTransportFailure(Throwable ex, Throwable cause) { + 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.trim().isEmpty()) { + message = ex.getMessage(); + } + return message; + } + + public static List handleBulkDeleteTokensBatchException( + Throwable ex, + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch, + int batchNumber, int batchSize + ) { + 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; + if (responseBody != null) { + if (responseBody.containsKey("tokens")) { + Object tokensList = responseBody.get("tokens"); + if (tokensList instanceof List) { + List recordsList = (List) tokensList; + for (int position = 0; position < recordsList.size(); position++) { + Object record = recordsList.get(position); + if (record instanceof Map) { + Map recordMap = (Map) record; + errorRecords.add(createDeleteTokensErrorRecord(recordMap, + startIndex + position, tokenAt(batchTokens, position), requestId)); + } + } + } + } 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; + 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()) { + for (int position = 0; position < batchTokens.size(); position++) { + errorRecords.add(new BulkDeleteTokensResponseRecord( + startIndex + position, tokenAt(batchTokens, position), + apiException.statusCode(), apiException.getMessage(), requestId)); + } + } + } else { + // a transport-level failure never reached the API, so there is no id to report + String message = describeTransportFailure(ex, cause); + for (int position = 0; position < batchTokens.size(); position++) { + errorRecords.add(new BulkDeleteTokensResponseRecord( + startIndex + position, tokenAt(batchTokens, position), 500, message)); + } + } + return errorRecords; + } + + 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) { + // Read through the shared helpers rather than casting: recordMap holds deserialised JSON, + // so a status can arrive as Double or String depending on the parser, and a blind + // (Integer) cast would turn a real API error into a ClassCastException. + int code = readHttpCode(recordMap, 500); + String message = readErrorMessage(recordMap); + String token = readString(recordMap, "value"); + if (token == null) { + token = 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; + // 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 { + // a transport-level failure never reached the API, so there is no id to report + httpCode = 500; + message = describeTransportFailure(ex, cause); + } + // 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(); + } + + public static BulkInsertResponse formatBulkInsertResponse(V1InsertResponse response, int batch, int batchSize, Map> headers) { + BulkInsertResponse formattedResponse = null; + List records = new ArrayList<>(); + if (response != null && response.getRecords().isPresent()) { + List record = response.getRecords().get(); + int indexNumber = batch * batchSize; + 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), + current.getSkyflowId().orElse(null), + current.getTokens().orElse(null), + current.getHashedData().orElse(null), + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null), + reqID)); + indexNumber++; + } + formattedResponse = new BulkInsertResponse(records); + } + return formattedResponse; + } + + public static BulkDetokenizeResponse formatBulkDetokenizeResponse(V1FlowDetokenizeResponse response, int batch, int batchSize, Map> headers) { + if (response != null && response.getResponse().isPresent()) { + List record = response.getResponse().get(); + List records = new ArrayList<>(); + int indexNumber = batch * batchSize; + 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), + data, + current.getHttpCode().orElse(current.getError().isPresent() ? 500 : 200), + current.getError().orElse(null), + reqID)); + indexNumber++; + } + return new BulkDetokenizeResponse(records); + } + return null; + } + + public static BulkDeleteTokensResponse formatBulkDeleteTokensResponse( + V1FlowDeleteTokenResponse response, + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batchRequest, + int batch, int batchSize, Map> headers) { + if (response != null && response.getTokens().isPresent()) { + List records = response.getTokens().get(); + List requestedTokens = batchRequest != null && batchRequest.getTokens().isPresent() + ? batchRequest.getTokens().get() : null; + List responseRecords = new ArrayList<>(); + int indexNumber = batch * batchSize; + // 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(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, + 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); + 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 new file mode 100644 index 00000000..667028ce --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -0,0 +1,490 @@ +package com.skyflow.utils.validations; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.InterfaceName; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.errors.ErrorCode; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.logs.ErrorLogs; +import com.skyflow.utils.Constants; +import com.skyflow.utils.Utils; +import com.skyflow.utils.logger.LogUtil; +import com.skyflow.vault.data.*; + +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; + +public class Validations extends BaseValidations { + private Validations() { + super(); + } + + public static void validateCredentials(Credentials credentials) throws SkyflowException { + int nonNullMembers = 0; + String path = credentials.getPath(); + String credentialsString = credentials.getCredentialsString(); + String token = credentials.getToken(); + String apiKey = credentials.getApiKey(); + Object context = credentials.getContext(); + ArrayList roles = credentials.getRoles(); + + if (path != null) nonNullMembers++; + if (credentialsString != null) nonNullMembers++; + if (token != null) nonNullMembers++; + if (apiKey != null) nonNullMembers++; + + if (nonNullMembers > 1) { + LogUtil.printErrorLog(ErrorLogs.MULTIPLE_TOKEN_GENERATION_MEANS_PASSED.getLog()); + throw new SkyflowException( + ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MultipleTokenGenerationMeansPassed.getMessage() + ); + } else if (nonNullMembers < 1) { + LogUtil.printErrorLog(ErrorLogs.NO_TOKEN_GENERATION_MEANS_PASSED.getLog()); + throw new SkyflowException( + ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NoTokenGenerationMeansPassed.getMessage() + ); + } else if (path != null && path.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_PATH.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialFilePath.getMessage()); + } else if (credentialsString != null && credentialsString.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_STRING.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialsString.getMessage()); + } else if (token != null && token.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_TOKEN_VALUE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyToken.getMessage()); + } else if (apiKey != null) { + if (apiKey.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_API_KEY_VALUE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyApikey.getMessage()); + } else { + Pattern pattern = Pattern.compile(Constants.API_KEY_REGEX); + Matcher matcher = pattern.matcher(apiKey); + if (!matcher.matches()) { + LogUtil.printErrorLog(ErrorLogs.INVALID_API_KEY.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidApikey.getMessage()); + } + } + } else if (roles != null) { + if (roles.isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_ROLES.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoles.getMessage()); + } else { + for (int index = 0; index < roles.size(); index++) { + String role = roles.get(index); + if (role == null || role.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_ROLE_IN_ROLES.getLog(), Integer.toString(index) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoleInRoles.getMessage()); + } + } + } + } + if (context != null) { + if (context instanceof String) { + String ctxStr = (String) context; + if (ctxStr.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } + } else if (context instanceof Map) { + Map ctxMap = (Map) context; + if (ctxMap.isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); + } + Pattern ctxKeyPattern = Pattern.compile(Constants.CONTEXT_KEY_REGEX); + for (Object key : ctxMap.keySet()) { + if (key == null || !ctxKeyPattern.matcher(key.toString()).matches()) { + String keyStr = key == null ? "null" : key.toString(); + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_CONTEXT_MAP_KEY.getLog(), keyStr)); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + Utils.parameterizedString(ErrorMessage.InvalidContextMapKey.getMessage(), keyStr)); + } + } + } else { + LogUtil.printErrorLog(ErrorLogs.INVALID_CONTEXT_TYPE.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidContextType.getMessage()); + } + } + } + + + public static void validateVaultConfiguration(VaultConfig vaultConfig) throws SkyflowException { + String vaultId = vaultConfig.getVaultId(); + String clusterId = vaultConfig.getClusterId(); + String vaultUrl = vaultConfig.getVaultUrl(); + Credentials credentials = vaultConfig.getCredentials(); + + if (vaultId == null) { + LogUtil.printErrorLog(ErrorLogs.VAULT_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultId.getMessage()); + } else if (vaultId.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_ID.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultId.getMessage()); + } else if (credentials != null) { + validateCredentials(credentials); + } + + if (vaultUrl != null) { + if (vaultUrl.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_VAULT_URL.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyVaultUrl.getMessage()); + } else if (!Utils.isValidUrl(vaultUrl)) { + LogUtil.printErrorLog(ErrorLogs.INVALID_VAULT_URL_FORMAT.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidVaultUrlFormat.getMessage()); + } + } else if (Utils.getEnvVaultUrl() == null) { + if (clusterId == null) { + LogUtil.printErrorLog(ErrorLogs.EITHER_VAULT_URL_OR_CLUSTER_ID_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EitherVaultUrlOrClusterIdRequired.getMessage()); + } else if (clusterId.trim().isEmpty()) { + LogUtil.printErrorLog(ErrorLogs.EMPTY_CLUSTER_ID.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyClusterId.getMessage()); + } + } + } + + + public static void validateInsertRequest(InsertRequest insertRequest) throws SkyflowException { + if (insertRequest == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INSERT_REQUEST_NULL.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InsertRequestNull.getMessage()); + } + List records = insertRequest.getRecords(); + if (records == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.RECORDS_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.RecordsKeyError.getMessage()); + } else if (records.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_RECORDS.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRecords.getMessage()); + } + + for (InsertRequestRecord record : records) { + if (record == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_RECORD.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRecord.getMessage()); + } + validateUpsertOptions(record.getUpsert()); + } + validateUpsertOptions(insertRequest.getUpsert()); + validateTableAndUpsertPlacement(insertRequest, records); + + for (InsertRequestRecord record : records) { + if (record.getData() != null) { + for (String key : record.getData().keySet()) { + if (key == null || key.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInRecords.getMessage()); + } else { + Object value = record.getData().get(key); + if (value == null || value.toString().trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), + InterfaceName.INSERT.getName(), key + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInValues.getMessage()); + } + } + } + } + validateInsertRecordTokens(record.getTokens()); + } + } + + // Tokens are optional on an insert record, but when supplied the map must not be empty and + // every entry must have a non-blank key and value — mirroring the checks on data above. + private static void validateInsertRecordTokens(Map tokens) throws SkyflowException { + if (tokens == null) { + return; + } + if (tokens.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_TOKENS.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokens.getMessage()); + } + for (String key : tokens.keySet()) { + if (key == null || key.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_KEY_IN_TOKENS.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInTokens.getMessage()); + } + Object value = tokens.get(key); + if (value == null || value.toString().trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_VALUE_IN_TOKENS.getLog(), + InterfaceName.INSERT.getName(), key + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokens.getMessage()); + } + } + } + + /** + * Table name must live at exactly one level — either on the request, or on every record. + * Upsert is optional, but wherever it is supplied it must sit at the same level as the + * table name; it need not appear on every record. + */ + private static void validateTableAndUpsertPlacement( + InsertRequest insertRequest, List records) throws SkyflowException { + boolean tableAtRequest = hasText(insertRequest.getTableName()); + boolean upsertAtRequest = insertRequest.getUpsert() != null; + + int recordsWithTable = 0; + boolean upsertAtRecords = false; + for (InsertRequestRecord record : records) { + if (hasText(record.getTableName())) recordsWithTable++; + if (record.getUpsert() != null) upsertAtRecords = true; + } + boolean tableAtRecords = recordsWithTable > 0; + + // ── table name: exactly one level ──────────────────────────────────── + if (tableAtRequest && tableAtRecords) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage()); + } + if (!tableAtRequest && recordsWithTable != records.size()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TABLE_NOT_SPECIFIED_AT_BOTH_PLACE.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage()); + } + + // ── upsert (optional) must match the table name's level ────────────── + if (upsertAtRecords && !tableAtRecords) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.UPSERT_TABLE_REQUEST_AT_RECORD_LEVEL.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage()); + } + if (upsertAtRequest && !tableAtRequest) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.UPSERT_TABLE_REQUEST_AT_REQUEST_LEVEL.getLog(), InterfaceName.INSERT.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage()); + } + } + + private static boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private static void validateUpsertOptions(UpsertOptions upsert) throws SkyflowException { + if (upsert == null) { + return; + } + if (upsert.getUniqueColumns() == null || upsert.getUniqueColumns().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_UPSERT_VALUES.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyUpsertValues.getMessage()); + } + // updateType is a free-form String on the request, but only the wire enum's values reach + // the wire. Reject anything else here rather than silently dropping it during mapping. + String updateType = upsert.getUpdateType(); + if (updateType != null && !isKnownUpdateType(updateType)) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_UPSERT_UPDATE_TYPE.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), + ErrorMessage.InvalidUpsertUpdateType.getMessage()); + } + } + + private static boolean isKnownUpdateType(String updateType) { + for (FlowEnumUpdateType type : FlowEnumUpdateType.values()) { + if (type.toString().equalsIgnoreCase(updateType)) { + return true; + } + } + return false; + } + + public static void validateDetokenizeRequest(DetokenizeRequest request) throws SkyflowException { + if (request == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.DETOKENIZE_REQUEST_NULL.getLog(), InterfaceName.DETOKENIZE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DetokenizeRequestNull.getMessage()); + } + List tokens = request.getTokens(); + if (tokens == null || tokens.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_DETOKENIZE_DATA.getLog(), InterfaceName.DETOKENIZE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDetokenizeData.getMessage()); + } + + for (int index = 0; index < tokens.size(); index++) { + String token = tokens.get(index); + if (token == null || token.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_TOKEN_IN_DETOKENIZE_DATA.getLog(), + InterfaceName.DETOKENIZE.getName(), + String.valueOf(index))); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenInDetokenizeData.getMessage()); + } + } + + List groupRedactions = request.getTokenGroupRedactions(); + if (groupRedactions != null && !groupRedactions.isEmpty()) { + for (TokenGroupRedactions group : groupRedactions) { + if (group == null) { + LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.NULL_TOKEN_REDACTION_GROUP_OBJECT.getLog(), InterfaceName.DETOKENIZE.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullTokenGroupRedactions.getMessage()); + } + String groupName = group.getTokenGroupName(); + String redaction = group.getRedaction(); + if (groupName == null || groupName.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.NULL_TOKEN_GROUP_NAME_IN_TOKEN_GROUP.getLog(), InterfaceName.DETOKENIZE.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullTokenGroupNameInTokenGroup.getMessage()); + } + if (redaction == null || redaction.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.EMPTY_OR_NULL_REDACTION_IN_TOKEN_GROUP.getLog(), InterfaceName.DETOKENIZE.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NullRedactionInTokenGroup.getMessage()); + } + } + } + + } + + // ── Bulk (batched/concurrent) request validations ──────────────────────── + + // BulkInsertRequest is an InsertRequest with no extra state, so the field rules are identical. + // The one addition: records must be BulkInsertRequestRecord, since BulkInsertResponse hands + // them back as such from getRecordsToRetry(). `records` is typed to the parent (it is + // inherited), so this is enforced here rather than by the compiler. + // The service accepts at most Constants.MAX_BULK_DATA_SIZE items per bulk call; batching + // splits the payload but does not lift that ceiling. + private static void validateBulkDataSize( + int size, ErrorLogs log, ErrorMessage message, InterfaceName interfaceName) throws SkyflowException { + if (size > Constants.MAX_BULK_DATA_SIZE) { + LogUtil.printErrorLog(Utils.parameterizedString(log.getLog(), interfaceName.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), message.getMessage()); + } + } + + public static void validateBulkInsertRequest(BulkInsertRequest insertRequest) throws SkyflowException { + validateInsertRequest(insertRequest); + validateBulkDataSize(insertRequest.getRecords().size(), ErrorLogs.RECORD_SIZE_EXCEED, + ErrorMessage.RecordSizeExceedError, InterfaceName.INSERT); + + for (InsertRequestRecord record : insertRequest.getRecords()) { + if (!(record instanceof BulkInsertRequestRecord)) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.INVALID_RECORD.getLog(), InterfaceName.INSERT.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRecord.getMessage()); + } + } + } + + // BulkDetokenizeRequest is a DetokenizeRequest with no extra state, so the rules are identical. + public static void validateBulkDetokenizeRequest(BulkDetokenizeRequest request) throws SkyflowException { + validateDetokenizeRequest(request); + validateBulkDataSize(request.getTokens().size(), ErrorLogs.TOKENS_SIZE_EXCEED, + ErrorMessage.TokensSizeExceedError, InterfaceName.DETOKENIZE); + } + + public static void validateBulkDeleteTokensRequest(BulkDeleteTokensRequest request) throws SkyflowException { + if (request == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.DELETE_TOKENS_REQUEST_NULL.getLog(), InterfaceName.DELETE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.DeleteTokensRequestNull.getMessage()); + } + List tokens = request.getTokens(); + if (tokens == null || tokens.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_DELETE_TOKENS_DATA.getLog(), InterfaceName.DELETE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyDeleteTokensData.getMessage()); + } + validateBulkDataSize(tokens.size(), ErrorLogs.DELETE_TOKENS_SIZE_EXCEED, + ErrorMessage.DeleteTokensSizeExceedError, InterfaceName.DELETE); + + for (int index = 0; index < tokens.size(); index++) { + String token = tokens.get(index); + if (token == null || token.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_OR_NULL_TOKEN_IN_DELETE_TOKENS_DATA.getLog(), + InterfaceName.DELETE.getName(), + String.valueOf(index))); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenInDeleteTokensData.getMessage()); + } + } + } + + public static void validateBulkTokenizeRequest(BulkTokenizeRequest request) throws SkyflowException { + if (request == null) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.TOKENIZE_REQUEST_NULL.getLog(), InterfaceName.TOKENIZE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokenizeRequestNull.getMessage()); + } + 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(records.size(), ErrorLogs.TOKENIZE_DATA_SIZE_EXCEED, + ErrorMessage.TokenizeDataSizeExceedError, InterfaceName.TOKENIZE); + + 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() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.TokenizeRecordNull.getMessage()); + } + Object value = record.getValue(); + boolean isInvalid = value == null + || (value instanceof String && ((String) value).trim().isEmpty()); + if (isInvalid) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_VALUE_IN_TOKENIZE_RECORD.getLog(), InterfaceName.TOKENIZE.getName() + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokenizeRecord.getMessage()); + } + List tokenGroupNames = record.getTokenGroupNames(); + if (tokenGroupNames != null) { + for (int j = 0; j < tokenGroupNames.size(); j++) { + String groupName = tokenGroupNames.get(j); + if (groupName == null || groupName.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_TOKEN_GROUP_NAME_IN_TOKENIZE_RECORD.getLog(), + InterfaceName.TOKENIZE.getName(), + String.valueOf(j) + )); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyTokenGroupNameInTokenizeRecord.getMessage()); + } + } + } + } + } + +} diff --git a/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java new file mode 100644 index 00000000..5ece5377 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -0,0 +1,961 @@ +package com.skyflow.vault.controller; + +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; +import java.util.concurrent.Executors; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.skyflow.VaultClient; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.core.ApiClientApiException; +import com.skyflow.generated.rest.core.ApiClientHttpResponse; +import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.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; +import com.skyflow.logs.WarningLogs; +import com.skyflow.utils.Constants; +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; +import com.skyflow.vault.data.BulkDetokenizeResponseRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkInsertResponseRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeResponse; +import com.skyflow.vault.data.BulkDetokenizeOptions; +import com.skyflow.vault.data.BulkInsertOptions; +import com.skyflow.vault.data.DeleteTokensOptions; +import com.skyflow.vault.data.ErrorRecord; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.RequestContext; +import com.skyflow.vault.data.RequestInterceptor; +import com.skyflow.vault.data.TokenizeOptions; + +import io.github.cdimascio.dotenv.Dotenv; +import io.github.cdimascio.dotenv.DotenvException; + +public final class VaultController extends VaultClient { + private static final Gson gson = new GsonBuilder().serializeNulls().create(); + private JsonObject metrics = Utils.getMetrics(); + + public VaultController(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException { + super(vaultConfig, credentials); + } + + /** + * Immutable per-call batch size / concurrency limit. Computed fresh on every bulk call + * instead of being stored on instance fields, since a VaultController instance is cached + * and reused (see Skyflow#vaultClientsMap) and may be invoked concurrently from multiple + * threads. + */ + private static final class BatchConfig { + final int batchSize; + final int concurrencyLimit; + + BatchConfig(int batchSize, int concurrencyLimit) { + this.batchSize = batchSize; + this.concurrencyLimit = concurrencyLimit; + } + } + + private RequestOptions buildRequestOptions(RequestContext context) { + RequestOptions.Builder builder = RequestOptions.builder() + .addHeader(Constants.SDK_METRICS_HEADER_KEY, metrics.toString()); + context.getHeaders().forEach((k, v) -> builder.addHeader(k.toString(), v)); + return builder.build(); + } + + // ── Bulk Insert ─────────────────────────────────────────────────────────── + + public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest) throws SkyflowException { + return bulkInsert(insertRequest, null); + } + + public BulkInsertResponse bulkInsert(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); + Validations.validateBulkInsertRequest(insertRequest); + BatchConfig cfg = configureInsertConcurrencyAndBatchSize(insertRequest.getRecords().size()); + + setBearerToken(); + V1InsertRequest request = Utils.getBulkInsertRequestBody(insertRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processBulkInsertSync(request, insertRequest.getRecords(), interceptor, cfg); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } catch (ExecutionException e) { + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + Throwable cause = e.getCause(); + throw new SkyflowException(cause != null && cause.getMessage() != null ? cause.getMessage() : e.getMessage()); + } + } + + public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest) throws SkyflowException { + return bulkInsertAsync(insertRequest, null); + } + + public CompletableFuture bulkInsertAsync(BulkInsertRequest insertRequest, BulkInsertOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_INSERT_REQUEST.getLog()); + Validations.validateBulkInsertRequest(insertRequest); + BatchConfig cfg = configureInsertConcurrencyAndBatchSize(insertRequest.getRecords().size()); + + setBearerToken(); + V1InsertRequest request = Utils.getBulkInsertRequestBody(insertRequest, this.getVaultConfig()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + List> futures = this.insertBatchFutures(request, interceptor, cfg); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + List records = new ArrayList<>(); + + for (CompletableFuture future : futures) { + BulkInsertResponse futureResponse = future.join(); + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); + } + } + + return new BulkInsertResponse(records, insertRequest.getRecords()); + }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } + } + + // ── Bulk Detokenize ─────────────────────────────────────────────────────── + + public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest) throws SkyflowException { + return bulkDetokenize(detokenizeRequest, null); + } + + public BulkDetokenizeResponse bulkDetokenize(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); + Validations.validateBulkDetokenizeRequest(detokenizeRequest); + BatchConfig cfg = configureDetokenizeConcurrencyAndBatchSize(detokenizeRequest.getTokens().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = + Utils.getBulkDetokenizeRequestBody(detokenizeRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processBulkDetokenizeSync(request, detokenizeRequest.getTokens(), interceptor, cfg); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SkyflowException(e.getMessage()); + } catch (ExecutionException e) { + throw new SkyflowException(e.getMessage()); + } + } + + public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest) throws SkyflowException { + return bulkDetokenizeAsync(detokenizeRequest, null); + } + + public CompletableFuture bulkDetokenizeAsync(BulkDetokenizeRequest detokenizeRequest, BulkDetokenizeOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_TRIGGERED.getLog()); + ExecutorService executor = null; + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_DETOKENIZE_REQUEST.getLog()); + Validations.validateBulkDetokenizeRequest(detokenizeRequest); + BatchConfig cfg = configureDetokenizeConcurrencyAndBatchSize(detokenizeRequest.getTokens().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest request = + Utils.getBulkDetokenizeRequestBody(detokenizeRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + + List records = new ArrayList<>(); + + List batches = + Utils.createBulkDetokenizeBatches(request, cfg.batchSize); + + executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List> futures = this.detokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + for (CompletableFuture future : futures) { + BulkDetokenizeResponse futureResponse = future.join(); + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); + } + } + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); + return new BulkDetokenizeResponse(records, detokenizeRequest.getTokens()); + }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw e; + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + if (executor != null) executor.shutdown(); + } + } + + // ── Bulk Delete Tokens ──────────────────────────────────────────────────── + + public BulkDeleteTokensResponse bulkDeleteTokens(BulkDeleteTokensRequest deleteTokensRequest) throws SkyflowException { + return bulkDeleteTokens(deleteTokensRequest, null); + } + + 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()); + Validations.validateBulkDeleteTokensRequest(deleteTokensRequest); + BatchConfig cfg = configureDeleteTokensConcurrencyAndBatchSize(deleteTokensRequest.getTokens().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = + Utils.getBulkDeleteTokensRequestBody(deleteTokensRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processBulkDeleteTokensSync(request, deleteTokensRequest.getTokens(), interceptor, cfg); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (ExecutionException | InterruptedException e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } + } + + public CompletableFuture bulkDeleteTokensAsync(BulkDeleteTokensRequest deleteTokensRequest) throws SkyflowException { + return bulkDeleteTokensAsync(deleteTokensRequest, null); + } + + public CompletableFuture bulkDeleteTokensAsync(BulkDeleteTokensRequest deleteTokensRequest, BulkDeleteTokensOptions options) throws SkyflowException { + LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_TRIGGERED.getLog()); + ExecutorService executor = null; + try { + LogUtil.printInfoLog(InfoLogs.VALIDATE_DELETE_TOKENS_REQUEST.getLog()); + Validations.validateBulkDeleteTokensRequest(deleteTokensRequest); + BatchConfig cfg = configureDeleteTokensConcurrencyAndBatchSize(deleteTokensRequest.getTokens().size()); + setBearerToken(); + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest request = + Utils.getBulkDeleteTokensRequestBody(deleteTokensRequest, this.getVaultConfig().getVaultId()); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + + List responseRecords = Collections.synchronizedList(new ArrayList<>()); + + List batches = + Utils.createBulkDeleteTokensBatches(request, cfg.batchSize); + + executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List> futures = + this.deleteTokensBatchFutures(executor, batches, interceptor, cfg.batchSize); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + for (CompletableFuture future : futures) { + BulkDeleteTokensResponse futureResponse = future.join(); + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); + } + } + LogUtil.printInfoLog(InfoLogs.DELETE_TOKENS_REQUEST_RESOLVED.getLog()); + return new BulkDeleteTokensResponse( + sortByIndex(responseRecords), deleteTokensRequest.getTokens()); + }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw e; + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + if (executor != null) executor.shutdown(); + } + } + + // ── Bulk Tokenize ───────────────────────────────────────────────────────── + + public BulkTokenizeResponse bulkTokenize(BulkTokenizeRequest tokenizeRequest) throws SkyflowException { + return bulkTokenize(tokenizeRequest, null); + } + + 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.getRecords().size()); + setBearerToken(); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + return this.processBulkTokenizeSync(tokenizeRequest.getRecords(), interceptor, cfg); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw e; + } catch (ExecutionException | InterruptedException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } + } + + public CompletableFuture bulkTokenizeAsync(BulkTokenizeRequest tokenizeRequest) throws SkyflowException { + return bulkTokenizeAsync(tokenizeRequest, null); + } + + 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.getRecords().size()); + setBearerToken(); + RequestInterceptor interceptor = options != null ? options.getInterceptor() : null; + + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + + List responseRecords = Collections.synchronizedList(new ArrayList<>()); + + List> batches = + Utils.createBulkTokenizeBatches(tokenizeRequest.getRecords(), cfg.batchSize); + + executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List> futures = + 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 && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); + } + } + LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); + return new BulkTokenizeResponse( + sortTokenizeByIndex(responseRecords), tokenizeRequest.getRecords()); + }); + } catch (ApiClientApiException e) { + String bodyString = gson.toJson(e.body()); + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + } catch (SkyflowException e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw e; + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + if (executor != null) executor.shutdown(); + } + } + + // ── Bulk private helpers ────────────────────────────────────────────────── + + private BulkDeleteTokensResponse processBulkDeleteTokensSync( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest deleteTokensRequest, + List originalTokens, + RequestInterceptor interceptor, + BatchConfig cfg + ) throws ExecutionException, InterruptedException, SkyflowException { + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + List responseRecords = new ArrayList<>(); + ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List batches = + Utils.createBulkDeleteTokensBatches(deleteTokensRequest, cfg.batchSize); + try { + List> futures = + this.deleteTokensBatchFutures(executor, batches, interceptor, cfg.batchSize); + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + } + for (CompletableFuture future : futures) { + BulkDeleteTokensResponse futureResponse = future.get(); + if (futureResponse != null && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); + } + } + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + executor.shutdown(); + } + 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, + RequestInterceptor interceptor, + int batchSize) { + 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.V1FlowDeleteTokenRequest batch = batches.get(index); + RequestContext ctx = new RequestContext("DELETE_TOKENS", batchIndex, batches.size()); + if (interceptor != null) interceptor.intercept(ctx); + CompletableFuture future = CompletableFuture + .supplyAsync(() -> processDeleteTokensBatch(batch, ctx), executor) + .handle((result, ex) -> { + if (ex != null) { + List batchErrors = + Utils.handleBulkDeleteTokensBatchException(ex, batch, index, batchSize); + return new BulkDeleteTokensResponse(batchErrors); + } + return Utils.formatBulkDeleteTokensResponse( + result.body(), batch, index, batchSize, result.headers()); + }); + futures.add(future); + } + return futures; + } + + private ApiClientHttpResponse processDeleteTokensBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch, + RequestContext ctx) { + 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 = settingResolver.apply("DELETE_TOKENS_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("DELETE_TOKENS_CONCURRENCY_LIMIT"); + + if (userProvidedBatchSize != null) { + try { + int parsedBatchSize = Integer.parseInt(userProvidedBatchSize); + if (parsedBatchSize > Constants.MAX_DELETE_TOKENS_BATCH_SIZE) { + LogUtil.printWarningLog(WarningLogs.BATCH_SIZE_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxBatchSize = Math.min(parsedBatchSize, Constants.MAX_DELETE_TOKENS_BATCH_SIZE); + if (maxBatchSize > 0) { + batchSize = maxBatchSize; + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.DELETE_TOKENS_BATCH_SIZE; + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.DELETE_TOKENS_BATCH_SIZE; + } + } + + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + + if (userProvidedConcurrencyLimit != null) { + try { + int parsedConcurrencyLimit = Integer.parseInt(userProvidedConcurrencyLimit); + if (parsedConcurrencyLimit > Constants.MAX_DELETE_TOKENS_CONCURRENCY_LIMIT) { + LogUtil.printWarningLog(WarningLogs.CONCURRENCY_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxConcurrencyLimit = Math.min(parsedConcurrencyLimit, Constants.MAX_DELETE_TOKENS_CONCURRENCY_LIMIT); + if (maxConcurrencyLimit > 0) { + concurrencyLimit = Math.min(maxConcurrencyLimit, maxConcurrencyNeeded); + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } else { + concurrencyLimit = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (Exception e) { + batchSize = Constants.DELETE_TOKENS_BATCH_SIZE; + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + concurrencyLimit = Math.min(Constants.DELETE_TOKENS_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + return new BatchConfig(batchSize, concurrencyLimit); + } + + private BulkTokenizeResponse processBulkTokenizeSync( + List originalRecords, + RequestInterceptor interceptor, + BatchConfig cfg + ) throws ExecutionException, InterruptedException, SkyflowException { + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + List responseRecords = new ArrayList<>(); + ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List> batches = + Utils.createBulkTokenizeBatches(originalRecords, cfg.batchSize); + try { + List> futures = + this.tokenizeBatchFutures(executor, batches, interceptor); + try { + 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 && futureResponse.getRecords() != null) { + responseRecords.addAll(futureResponse.getRecords()); + } + } + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + executor.shutdown(); + } + 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) { + List> futures = new ArrayList<>(); + if (batches == null) return futures; + // 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; + int batchPosition = 0; + for (List batchRecords : batches) { + final int startIndex = nextStartIndex; + nextStartIndex += batchRecords.size(); + final int batchIndex = batchPosition++; + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + Utils.getBulkTokenizeRequestBody(batchRecords, this.getVaultConfig().getVaultId()); + RequestContext ctx = new RequestContext("TOKENIZE", batchIndex, batches.size()); + if (interceptor != null) interceptor.intercept(ctx); + CompletableFuture future = CompletableFuture + .supplyAsync(() -> processTokenizeBatch(batch, ctx), executor) + .handle((result, ex) -> { + if (ex != null) { + return new BulkTokenizeResponse(Utils.handleBulkTokenizeBatchException( + ex, batchRecords, startIndex)); + } + return Utils.formatBulkTokenizeResponse( + result.body(), batchRecords, startIndex, result.headers()); + }); + futures.add(future); + } + return futures; + } + + private ApiClientHttpResponse processTokenizeBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch, + RequestContext ctx) { + return this.getRecordsApi().withRawResponse().tokenize(batch, buildRequestOptions(ctx)); + } + + private BatchConfig configureTokenizeConcurrencyAndBatchSize(int totalRequests) { + int batchSize = Constants.TOKENIZE_BATCH_SIZE; + int concurrencyLimit; + try { + String userProvidedBatchSize = settingResolver.apply("TOKENIZE_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("TOKENIZE_CONCURRENCY_LIMIT"); + + if (userProvidedBatchSize != null) { + try { + int parsedBatchSize = Integer.parseInt(userProvidedBatchSize); + if (parsedBatchSize > Constants.MAX_TOKENIZE_BATCH_SIZE) { + LogUtil.printWarningLog(WarningLogs.BATCH_SIZE_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxBatchSize = Math.min(parsedBatchSize, Constants.MAX_TOKENIZE_BATCH_SIZE); + if (maxBatchSize > 0) { + batchSize = maxBatchSize; + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.TOKENIZE_BATCH_SIZE; + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.TOKENIZE_BATCH_SIZE; + } + } + + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + + if (userProvidedConcurrencyLimit != null) { + try { + int parsedConcurrencyLimit = Integer.parseInt(userProvidedConcurrencyLimit); + if (parsedConcurrencyLimit > Constants.MAX_TOKENIZE_CONCURRENCY_LIMIT) { + LogUtil.printWarningLog(WarningLogs.CONCURRENCY_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxConcurrencyLimit = Math.min(parsedConcurrencyLimit, Constants.MAX_TOKENIZE_CONCURRENCY_LIMIT); + if (maxConcurrencyLimit > 0) { + concurrencyLimit = Math.min(maxConcurrencyLimit, maxConcurrencyNeeded); + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } else { + concurrencyLimit = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (Exception e) { + batchSize = Constants.TOKENIZE_BATCH_SIZE; + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + concurrencyLimit = Math.min(Constants.TOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + return new BatchConfig(batchSize, concurrencyLimit); + } + + private BatchConfig configureDetokenizeConcurrencyAndBatchSize(int totalRequests) { + int batchSize = Constants.DETOKENIZE_BATCH_SIZE; + int concurrencyLimit; + try { + String userProvidedBatchSize = settingResolver.apply("DETOKENIZE_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("DETOKENIZE_CONCURRENCY_LIMIT"); + + if (userProvidedBatchSize != null) { + try { + int parsedBatchSize = Integer.parseInt(userProvidedBatchSize); + if (parsedBatchSize > Constants.MAX_DETOKENIZE_BATCH_SIZE) { + LogUtil.printWarningLog(WarningLogs.BATCH_SIZE_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxBatchSize = Math.min(parsedBatchSize, Constants.MAX_DETOKENIZE_BATCH_SIZE); + if (maxBatchSize > 0) { + batchSize = maxBatchSize; + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.DETOKENIZE_BATCH_SIZE; + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.DETOKENIZE_BATCH_SIZE; + } + } + + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + + if (userProvidedConcurrencyLimit != null) { + try { + int parsedConcurrencyLimit = Integer.parseInt(userProvidedConcurrencyLimit); + if (parsedConcurrencyLimit > Constants.MAX_DETOKENIZE_CONCURRENCY_LIMIT) { + LogUtil.printWarningLog(WarningLogs.CONCURRENCY_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxConcurrencyLimit = Math.min(parsedConcurrencyLimit, Constants.MAX_DETOKENIZE_CONCURRENCY_LIMIT); + + if (maxConcurrencyLimit > 0) { + concurrencyLimit = Math.min(maxConcurrencyLimit, maxConcurrencyNeeded); + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.DETOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.DETOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } else { + concurrencyLimit = Math.min(Constants.DETOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (Exception e) { + batchSize = Constants.DETOKENIZE_BATCH_SIZE; + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + concurrencyLimit = Math.min(Constants.DETOKENIZE_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + return new BatchConfig(batchSize, concurrencyLimit); + } + + private BulkInsertResponse processBulkInsertSync( + V1InsertRequest insertRequest, + List originalPayload, + RequestInterceptor interceptor, + BatchConfig cfg + ) throws ExecutionException, InterruptedException, SkyflowException { + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + List records = new ArrayList<>(); + List> futures = this.insertBatchFutures(insertRequest, interceptor, cfg); + + try { + CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + try { + allFutures.join(); + } catch (Exception e) { + // individual batch errors are already captured + } + for (CompletableFuture future : futures) { + BulkInsertResponse futureResponse = future.get(); + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); + } + } + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); + LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); + return response; + } + + private BulkDetokenizeResponse processBulkDetokenizeSync( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest detokenizeRequest, + List originalTokens, + RequestInterceptor interceptor, + BatchConfig cfg + ) throws ExecutionException, InterruptedException, SkyflowException { + LogUtil.printInfoLog(InfoLogs.PROCESSING_BATCHES.getLog()); + List records = new ArrayList<>(); + ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List batches = + Utils.createBulkDetokenizeBatches(detokenizeRequest, cfg.batchSize); + try { + List> futures = this.detokenizeBatchFutures(executor, batches, interceptor, cfg.batchSize); + try { + CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + allFutures.join(); + } catch (Exception e) { + // individual batch errors are already captured + } + for (CompletableFuture future : futures) { + BulkDetokenizeResponse futureResponse = future.get(); + if (futureResponse != null && futureResponse.getRecords() != null) { + records.addAll(futureResponse.getRecords()); + } + } + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); + throw new SkyflowException(e.getMessage()); + } finally { + executor.shutdown(); + } + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalTokens); + LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); + return response; + } + + private List> detokenizeBatchFutures( + ExecutorService executor, + List batches, + RequestInterceptor interceptor, + int batchSize) { + List> futures = new ArrayList<>(); + for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = batches.get(batchIndex); + int batchNumber = batchIndex; + RequestContext ctx = new RequestContext("DETOKENIZE", batchIndex, batches.size()); + if (interceptor != null) interceptor.intercept(ctx); + CompletableFuture future = CompletableFuture + .supplyAsync(() -> processDetokenizeBatch(batch, ctx), executor) + .thenApply(response -> Utils.formatBulkDetokenizeResponse(response.body(), batchNumber, batchSize, response.headers())) + .exceptionally(ex -> new BulkDetokenizeResponse( + Utils.handleBulkDetokenizeBatchException(ex, batch, batchNumber, batchSize))); + futures.add(future); + } + return futures; + } + + private ApiClientHttpResponse processDetokenizeBatch( + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch, + RequestContext ctx) { + return this.getRecordsApi().withRawResponse().detokenize(batch, buildRequestOptions(ctx)); + } + + private List> insertBatchFutures( + V1InsertRequest insertRequest, + RequestInterceptor interceptor, + BatchConfig cfg) { + List records = insertRequest.getRecords().get(); + + ExecutorService executor = Executors.newFixedThreadPool(cfg.concurrencyLimit); + List> batches = Utils.createBulkInsertBatches(records, cfg.batchSize); + List> futures = new ArrayList<>(); + + try { + for (int batchIndex = 0; batchIndex < batches.size(); batchIndex++) { + List batch = batches.get(batchIndex); + int batchNumber = batchIndex; + RequestContext ctx = new RequestContext("INSERT", batchIndex, batches.size()); + if (interceptor != null) interceptor.intercept(ctx); + CompletableFuture future = CompletableFuture + .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))); + futures.add(future); + } + } finally { + executor.shutdown(); + } + return futures; + } + + // 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)); + } + + private BatchConfig configureInsertConcurrencyAndBatchSize(int totalRequests) { + int batchSize = Constants.INSERT_BATCH_SIZE; + int concurrencyLimit; + try { + String userProvidedBatchSize = settingResolver.apply("INSERT_BATCH_SIZE"); + String userProvidedConcurrencyLimit = settingResolver.apply("INSERT_CONCURRENCY_LIMIT"); + + if (userProvidedBatchSize != null) { + try { + int parsedBatchSize = Integer.parseInt(userProvidedBatchSize); + if (parsedBatchSize > Constants.MAX_INSERT_BATCH_SIZE) { + LogUtil.printWarningLog(WarningLogs.BATCH_SIZE_EXCEEDS_MAX_LIMIT.getLog()); + } + int maxBatchSize = Math.min(parsedBatchSize, Constants.MAX_INSERT_BATCH_SIZE); + if (maxBatchSize > 0) { + batchSize = maxBatchSize; + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.INSERT_BATCH_SIZE; + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_BATCH_SIZE_PROVIDED.getLog()); + batchSize = Constants.INSERT_BATCH_SIZE; + } + } + + // Max no of threads required to run all batches concurrently at once + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + + if (userProvidedConcurrencyLimit != null) { + try { + int parsedConcurrencyLimit = Integer.parseInt(userProvidedConcurrencyLimit); + int maxConcurrencyLimit = Math.min(parsedConcurrencyLimit, Constants.MAX_INSERT_CONCURRENCY_LIMIT); + if (parsedConcurrencyLimit > Constants.MAX_INSERT_CONCURRENCY_LIMIT) { + LogUtil.printWarningLog(WarningLogs.CONCURRENCY_EXCEEDS_MAX_LIMIT.getLog()); + } + if (maxConcurrencyLimit > 0) { + concurrencyLimit = Math.min(maxConcurrencyLimit, maxConcurrencyNeeded); + } else { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.INSERT_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (NumberFormatException e) { + LogUtil.printWarningLog(WarningLogs.INVALID_CONCURRENCY_LIMIT_PROVIDED.getLog()); + concurrencyLimit = Math.min(Constants.INSERT_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } else { + concurrencyLimit = Math.min(Constants.INSERT_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + } catch (Exception e) { + batchSize = Constants.INSERT_BATCH_SIZE; + int maxConcurrencyNeeded = (totalRequests + batchSize - 1) / batchSize; + concurrencyLimit = Math.min(Constants.INSERT_CONCURRENCY_LIMIT, maxConcurrencyNeeded); + } + return new BatchConfig(batchSize, concurrencyLimit); + } + +} 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 new file mode 100644 index 00000000..5d123922 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensRequest.java @@ -0,0 +1,30 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class BulkDeleteTokensRequest extends DeleteTokensRequest { + + private BulkDeleteTokensRequest(List tokens) { + super(tokens); + } + + public static BulkDeleteTokensRequestBuilder builder() { + return new BulkDeleteTokensRequestBuilder(); + } + + 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.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 new file mode 100644 index 00000000..d4530145 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDeleteTokensResponse.java @@ -0,0 +1,91 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +public class BulkDeleteTokensResponse { + @Expose(serialize = true) + private DeleteTokensSummary summary; + + @Expose(serialize = true) + private List records; + + private List originalPayload; + private List tokensToRetry; + + public BulkDeleteTokensResponse(List records) { + this.records = records; + } + + public BulkDeleteTokensResponse(List records, List originalPayload) { + this.records = records; + this.originalPayload = originalPayload; + 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 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; + } + + 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() + .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/BulkDetokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java new file mode 100644 index 00000000..dd2c9366 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeRequest.java @@ -0,0 +1,39 @@ +package com.skyflow.vault.data; + +import java.util.List; + +// Bulk counterpart of DetokenizeRequest. Carries no extra state today; all fields +// (tokens, tokenGroupRedactions) are inherited. +public class BulkDetokenizeRequest extends DetokenizeRequest { + + protected BulkDetokenizeRequest(BulkDetokenizeRequestBuilder builder) { + super(builder); + } + + public static BulkDetokenizeRequestBuilder builder() { + return new BulkDetokenizeRequestBuilder(); + } + + public static final class BulkDetokenizeRequestBuilder extends DetokenizeRequestBuilder { + + private BulkDetokenizeRequestBuilder() { + } + + @Override + public BulkDetokenizeRequestBuilder tokens(List tokens) { + super.tokens(tokens); + return this; + } + + @Override + public BulkDetokenizeRequestBuilder tokenGroupRedactions(List tokenGroupRedactions) { + super.tokenGroupRedactions(tokenGroupRedactions); + return this; + } + + @Override + public BulkDetokenizeRequest build() { + return new BulkDetokenizeRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java new file mode 100644 index 00000000..279e1dc6 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponse.java @@ -0,0 +1,59 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BulkDetokenizeResponse { + private DetokenizeSummary summary; + private List records; + + // Internal fields. transient keeps them out of the toString() output. + private transient List originalPayload; + private transient List tokensToRetry; + + public BulkDetokenizeResponse(List records) { + this.records = records; + } + + public BulkDetokenizeResponse( + List records, + List originalPayload + ) { + this.records = records; + this.originalPayload = originalPayload; + int totalFailed = (int) records.stream().filter(record -> record.getError() != null).count(); + this.summary = new DetokenizeSummary(originalPayload.size(), records.size() - totalFailed, totalFailed); + } + + public DetokenizeSummary getSummary() { + return this.summary; + } + + public List getRecords() { + return this.records; + } + + public List getTokensToRetry() { + if (tokensToRetry == null) { + // Per-batch responses are built without the original payload; nothing to retry from. + if (originalPayload == null) { + return new ArrayList<>(); + } + tokensToRetry = records.stream() + .filter(record -> record.getHttpCode() >= 500 && record.getHttpCode() <= 599 + && record.getHttpCode() != 529) + .map(record -> originalPayload.get(record.getIndex())) + .collect(Collectors.toCollection(ArrayList::new)); + } + return tokensToRetry; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java new file mode 100644 index 00000000..e38c20d7 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkDetokenizeResponseRecord.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.Map; + +// Bulk counterpart of DetokenizeResponseRecord. Adds the caller-facing position of the token +// in the submitted payload; all other fields are inherited. +public class BulkDetokenizeResponseRecord extends DetokenizeResponseRecord { + private final int index; + private final String requestId; + + public BulkDetokenizeResponseRecord(int index, String token, Object value, String tokenGroupName, + Map metadata, int httpCode, String error, + String requestId) { + super(token, value, tokenGroupName, metadata, httpCode, error); + this.index = index; + this.requestId = requestId; + } + + public int getIndex() { + return index; + } + + public String getRequestId() { + return requestId; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(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/BulkInsertRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java new file mode 100644 index 00000000..82ff45ef --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequest.java @@ -0,0 +1,45 @@ +package com.skyflow.vault.data; + +import java.util.List; + +// Bulk counterpart of InsertRequest. Carries no extra state today; all fields +// (tableName, records, upsert) are inherited. +public class BulkInsertRequest extends InsertRequest { + + protected BulkInsertRequest(BulkInsertRequestBuilder builder) { + super(builder); + } + + public static BulkInsertRequestBuilder builder() { + return new BulkInsertRequestBuilder(); + } + + public static final class BulkInsertRequestBuilder extends InsertRequestBuilder { + + private BulkInsertRequestBuilder() { + } + + @Override + public BulkInsertRequestBuilder tableName(String tableName) { + super.tableName(tableName); + return this; + } + + @Override + public BulkInsertRequestBuilder records(List records) { + super.records(records); + return this; + } + + @Override + public BulkInsertRequestBuilder upsert(UpsertOptions upsert) { + super.upsert(upsert); + return this; + } + + @Override + public BulkInsertRequest build() { + return new BulkInsertRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java new file mode 100644 index 00000000..6cddda6b --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertRequestRecord.java @@ -0,0 +1,51 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +// Bulk counterpart of InsertRequestRecord. Carries no extra state today; it exists so +// bulk-only fields can be added without touching the unary record. +public class BulkInsertRequestRecord extends InsertRequestRecord { + + protected BulkInsertRequestRecord(BulkInsertRequestRecordBuilder builder) { + super(builder); + } + + public static BulkInsertRequestRecordBuilder builder() { + return new BulkInsertRequestRecordBuilder(); + } + + public static final class BulkInsertRequestRecordBuilder extends InsertRequestRecordBuilder { + + private BulkInsertRequestRecordBuilder() { + } + + @Override + public BulkInsertRequestRecordBuilder tableName(String tableName) { + super.tableName(tableName); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder data(Map data) { + super.data(data); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder tokens(Map tokens) { + super.tokens(tokens); + return this; + } + + @Override + public BulkInsertRequestRecordBuilder upsert(UpsertOptions upsert) { + super.upsert(upsert); + return this; + } + + @Override + public BulkInsertRequestRecord build() { + return new BulkInsertRequestRecord(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java new file mode 100644 index 00000000..b97d2e97 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponse.java @@ -0,0 +1,61 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class BulkInsertResponse { + private BulkSummary summary; + private List records; + + // Internal fields. transient keeps them out of the toString() output. + private transient List originalPayload; + private transient List recordsToRetry; + + public BulkInsertResponse(List records) { + this.records = records; + } + + public BulkInsertResponse( + List records, + List originalPayload + ) { + this.records = records; + this.originalPayload = originalPayload; + int totalFailed = (int) records.stream().filter(record -> record.getError() != null).count(); + this.summary = new BulkSummary(originalPayload.size(), records.size() - totalFailed, totalFailed); + } + + public BulkSummary getSummary() { + return this.summary; + } + + public List getRecords() { + return this.records; + } + + // Records are guaranteed to be BulkInsertRequestRecord: validateBulkInsertRequest rejects + // any other InsertRequestRecord subtype before the request is ever sent. + public List getRecordsToRetry() { + if (recordsToRetry == null) { + // Per-batch responses are built without the original payload; nothing to retry from. + if (originalPayload == null) { + return new ArrayList<>(); + } + recordsToRetry = records.stream() + .filter(record -> record.getHttpCode() >= 500 && record.getHttpCode() <= 599 + && record.getHttpCode() != 529) + .map(record -> (BulkInsertRequestRecord) originalPayload.get(record.getIndex())) + .collect(Collectors.toCollection(ArrayList::new)); + } + return recordsToRetry; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java new file mode 100644 index 00000000..1ea6b757 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkInsertResponseRecord.java @@ -0,0 +1,34 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.Map; + +// Bulk counterpart of InsertResponseRecord. Adds the caller-facing position of the record +// in the submitted payload; all other fields are inherited. +public class BulkInsertResponseRecord extends InsertResponseRecord { + private final int index; + private final String requestId; + + public BulkInsertResponseRecord(int index, String tableName, String skyflowId, + Map fields, Map hashedData, + int httpCode, String error, String requestId) { + super(tableName, skyflowId, fields, hashedData, httpCode, error); + this.index = index; + this.requestId = requestId; + } + + public int getIndex() { + return index; + } + + public String getRequestId(){ + return requestId; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java new file mode 100644 index 00000000..f32a14ee --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkSummary.java @@ -0,0 +1,40 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +public class BulkSummary { + @Expose(serialize = true) + private int totalRecords; + @Expose(serialize = true) + private int totalInserted; + @Expose(serialize = true) + private int totalFailed; + + public BulkSummary() { + } + + public BulkSummary(int totalRecords, int totalInserted, int totalFailed) { + this.totalRecords = totalRecords; + this.totalInserted = totalInserted; + this.totalFailed = totalFailed; + } + + public int getTotalRecords() { + return totalRecords; + } + + public int getTotalInserted() { + return totalInserted; + } + + public int getTotalFailed() { + return totalFailed; + } + + @Override + public String toString() { + Gson gson = new Gson(); + return gson.toJson(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/BulkTokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java new file mode 100644 index 00000000..852f00d9 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeRequest.java @@ -0,0 +1,50 @@ +package com.skyflow.vault.data; + +import java.util.ArrayList; +import java.util.List; + +public class BulkTokenizeRequest extends TokenizeRequest { + + private BulkTokenizeRequest(List records) { + super(records); + } + + public static BulkTokenizeRequestBuilder builder() { + return new BulkTokenizeRequestBuilder(); + } + + /** + * 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 extends TokenizeRequestBuilder { + + private BulkTokenizeRequestBuilder() {} + + @Override + public BulkTokenizeRequestBuilder records(List records) { + this.records = records; + return this; + } + + @Override + public BulkTokenizeRequest build() { + 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 new file mode 100644 index 00000000..115fb9b3 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/BulkTokenizeResponse.java @@ -0,0 +1,130 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +import java.util.ArrayList; +import java.util.List; + +public class BulkTokenizeResponse { + @Expose(serialize = true) + private TokenizeSummary summary; + + @Expose(serialize = true) + private List records; + + private List originalPayload; + private List recordsToRetry; + + public BulkTokenizeResponse(List records) { + this.records = records; + } + + public BulkTokenizeResponse(List records, + List originalPayload) { + this.records = records; + this.originalPayload = originalPayload; + this.summary = buildSummary(this.records, this.originalPayload); + } + + /** + * {@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; + 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++; + } + } + } + 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 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; + } + + 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() + .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 new file mode 100644 index 00000000..67a3e849 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensOptions.java @@ -0,0 +1,38 @@ +package com.skyflow.vault.data; + +/** + * 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; + + protected DeleteTokensOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() {} + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public DeleteTokensOptions build() { + return new DeleteTokensOptions(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/DeleteTokensSummary.java b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensSummary.java new file mode 100644 index 00000000..92770caf --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DeleteTokensSummary.java @@ -0,0 +1,41 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +public class DeleteTokensSummary { + @Expose(serialize = true) + private int totalTokens; + + @Expose(serialize = true) + private int totalDeleted; + + @Expose(serialize = true) + private int totalFailed; + + public DeleteTokensSummary() {} + + public DeleteTokensSummary(int totalTokens, int totalDeleted, int totalFailed) { + this.totalTokens = totalTokens; + this.totalDeleted = totalDeleted; + this.totalFailed = totalFailed; + } + + public int getTotalTokens() { + return totalTokens; + } + + public int getTotalDeleted() { + return totalDeleted; + } + + public int getTotalFailed() { + return totalFailed; + } + + @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 new file mode 100644 index 00000000..ddc18bdf --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class DetokenizeOptions { + private final RequestInterceptor interceptor; + + protected DetokenizeOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public DetokenizeOptions build() { + return new DetokenizeOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java new file mode 100644 index 00000000..e833450e --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeRequest.java @@ -0,0 +1,41 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class DetokenizeRequest extends BaseDetokenizeRequest { + private final DetokenizeRequestBuilder builder; + + protected DetokenizeRequest(DetokenizeRequestBuilder builder) { + this.builder = builder; + } + + public static DetokenizeRequestBuilder builder() { + return new DetokenizeRequestBuilder(); + } + + public List getTokens() { + return this.builder.tokens; + } + + public List getTokenGroupRedactions(){ + return this.builder.tokenGroupRedactions; + } + + public static class DetokenizeRequestBuilder { + private List tokens; + private List tokenGroupRedactions; + + public DetokenizeRequestBuilder tokens(List tokens) { + this.tokens = tokens; + return this; + } + public DetokenizeRequestBuilder tokenGroupRedactions(List tokenGroupRedactions){ + this.tokenGroupRedactions = tokenGroupRedactions; + return this; + } + public DetokenizeRequest build() { + return new DetokenizeRequest(this); + } + } + +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java new file mode 100644 index 00000000..b716abca --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponse.java @@ -0,0 +1,25 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.List; + +// Response shape for the unary detokenize contract. Retained as published API even though the +// module currently exposes only the bulk operations. +public class DetokenizeResponse extends BaseDetokenizeResponse { + private final List records; + + public DetokenizeResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java new file mode 100644 index 00000000..e0aa8278 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeResponseRecord.java @@ -0,0 +1,36 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class DetokenizeResponseRecord extends BaseDetokenizeRecordResponse { + // Passed straight through from V1FlowDetokenizeResponseObject.getValue() (Optional). + private final Object value; + private final String tokenGroupName; + private final Map metadata; + private final int httpCode; + + public DetokenizeResponseRecord(String token, Object value, String tokenGroupName, + Map metadata, int httpCode, String error) { + super(token, error); + this.value = value; + this.tokenGroupName = tokenGroupName; + this.metadata = metadata; + this.httpCode = httpCode; + } + + public Object getValue() { + return value; + } + + public String getTokenGroupName() { + return tokenGroupName; + } + + public Map getMetadata() { + return metadata; + } + + public int getHttpCode() { + return httpCode; + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeSummary.java b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeSummary.java new file mode 100644 index 00000000..c86c1854 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/DetokenizeSummary.java @@ -0,0 +1,40 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +public class DetokenizeSummary { + @Expose(serialize = true) + private int totalTokens; + @Expose(serialize = true) + private int totalDetokenized; + @Expose(serialize = true) + private int totalFailed; + + public DetokenizeSummary() { + } + + public DetokenizeSummary(int totalTokens, int totalDetokenized, int totalFailed) { + this.totalTokens = totalTokens; + this.totalDetokenized = totalDetokenized; + this.totalFailed = totalFailed; + } + + public int getTotalTokens() { + return totalTokens; + } + + public int getTotalDetokenized() { + return totalDetokenized; + } + + public int getTotalFailed() { + return totalFailed; + } + + @Override + public String toString() { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/ErrorRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/ErrorRecord.java new file mode 100644 index 00000000..1536c0f9 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/ErrorRecord.java @@ -0,0 +1,51 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; + +public class ErrorRecord { + @Expose(serialize = true) + private int index; + @Expose(serialize = true) + private String error; + @Expose(serialize = true) + private int code; + @Expose(serialize = true) + private String requestId; + + public ErrorRecord(int index, String error, int code) { + this.index = index; + this.error = error; + this.code = code; + } + + public ErrorRecord(int index, String error, int code, String requestId) { + this.index = index; + this.error = error; + this.code = code; + this.requestId = requestId; + } + + public String getError() { + return error; + } + + public int getCode() { + return code; + } + + public int getIndex() { + return index; + } + + public String getRequestId() { + return requestId; + } + + + @Override + public String toString() { + Gson gson = new Gson(); + return gson.toJson(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 new file mode 100644 index 00000000..a3d5aa79 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertOptions.java @@ -0,0 +1,33 @@ +package com.skyflow.vault.data; + +public class InsertOptions { + private final RequestInterceptor interceptor; + + protected InsertOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() { + } + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public InsertOptions build() { + return new InsertOptions(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java new file mode 100644 index 00000000..4a2bcb5d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequest.java @@ -0,0 +1,55 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class InsertRequest extends BaseInsertRequest { + private final InsertRequestBuilder builder; + + protected InsertRequest(InsertRequestBuilder builder) { + this.builder = builder; + } + + public static InsertRequestBuilder builder() { + return new InsertRequestBuilder(); + } + + public String getTableName() { + return this.builder.tableName; + } + + public List getRecords() { + return this.builder.records; + } + + public UpsertOptions getUpsert() { + return this.builder.upsert; + } + + public static class InsertRequestBuilder { + private String tableName; + private List records; + private UpsertOptions upsert; + + protected InsertRequestBuilder() { + } + + public InsertRequestBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public InsertRequestBuilder records(List records) { + this.records = records; + return this; + } + + public InsertRequestBuilder upsert(UpsertOptions upsert) { + this.upsert = upsert; + return this; + } + + public InsertRequest build() { + return new InsertRequest(this); + } + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java new file mode 100644 index 00000000..75747a84 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertRequestRecord.java @@ -0,0 +1,65 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class InsertRequestRecord { + private final InsertRequestRecordBuilder builder; + + protected InsertRequestRecord(InsertRequestRecordBuilder builder) { + this.builder = builder; + } + + // Getters + public String getTableName() { + return this.builder.tableName; + } + + public Map getData() { + return this.builder.data; + } + + public Map getTokens() { + return this.builder.tokens; + } + + public UpsertOptions getUpsert() { + return this.builder.upsert; + } + + // Builder Class + public static class InsertRequestRecordBuilder { + private String tableName; + private Map data; + private Map tokens; + private UpsertOptions upsert; + + public InsertRequestRecordBuilder tableName(String tableName) { + this.tableName = tableName; + return this; + } + + public InsertRequestRecordBuilder data(Map data) { + this.data = data; + return this; + } + + public InsertRequestRecordBuilder tokens(Map tokens) { + this.tokens = tokens; + return this; + } + + public InsertRequestRecordBuilder upsert(UpsertOptions upsert) { + this.upsert = upsert; + return this; + } + + public InsertRequestRecord build() { + return new InsertRequestRecord(this); + } + } + + // Static entry point for builder + public static InsertRequestRecordBuilder builder() { + return new InsertRequestRecordBuilder(); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java new file mode 100644 index 00000000..594e5fd1 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponse.java @@ -0,0 +1,25 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; + +import java.util.List; + +// Response shape for the unary insert contract. Retained as published API even though the +// module currently exposes only the bulk operations. +public class InsertResponse extends BaseInsertResponse { + private final List records; + + public InsertResponse(List records) { + this.records = records; + } + + public List getRecords() { + return records; + } + + @Override + public String toString() { + Gson gson = new Gson().newBuilder().serializeNulls().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java new file mode 100644 index 00000000..442d3749 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/InsertResponseRecord.java @@ -0,0 +1,46 @@ +package com.skyflow.vault.data; + +import java.util.Map; + +public class InsertResponseRecord { + private final String tableName; + private final String skyflowId; + private final Map fields; + private final Map hashedData; + private final int httpCode; + private final String error; + + public InsertResponseRecord(String tableName, String skyflowId, Map fields, + Map hashedData, int httpCode, String error) { + this.tableName = tableName; + this.skyflowId = skyflowId; + this.fields = fields; + this.hashedData = hashedData; + this.httpCode = httpCode; + this.error = error; + } + + public String getTableName() { + return tableName; + } + + public String getSkyflowId() { + return skyflowId; + } + + public Map getFields() { + return fields; + } + + public Map getHashedData() { + return hashedData; + } + + public int getHttpCode() { + return httpCode; + } + + public String getError() { + return error; + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/RequestContext.java b/flowvault/src/main/java/com/skyflow/vault/data/RequestContext.java new file mode 100644 index 00000000..a5752f3a --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/RequestContext.java @@ -0,0 +1,47 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class RequestContext { + /** Reported when the caller's request was not split into batches. */ + private static final int NOT_BATCHED = -1; + + private final String operation; + private final int batchIndex; + private final int totalBatches; + private final Map headers = new HashMap<>(); + + public RequestContext(String operation) { + this(operation, NOT_BATCHED, NOT_BATCHED); + } + + public RequestContext(String operation, int batchIndex, int totalBatches) { + this.operation = operation; + this.batchIndex = batchIndex; + this.totalBatches = totalBatches; + } + + public String getOperation() { return operation; } + + /** + * Zero-based position of this batch within the caller's request, or -1 when the operation was + * not batched. Lets an interceptor tag each batch distinctly — a per-batch correlation id, for + * instance — instead of seeing an identical context for every one. + */ + public int getBatchIndex() { return batchIndex; } + + /** Total number of batches the request was split into, or -1 when it was not batched. */ + public int getTotalBatches() { return totalBatches; } + + public void addHeader(CustomHeaderKey key, String value) { + headers.put(key, value); + } + + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/RequestInterceptor.java b/flowvault/src/main/java/com/skyflow/vault/data/RequestInterceptor.java new file mode 100644 index 00000000..37f5261d --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/RequestInterceptor.java @@ -0,0 +1,6 @@ +package com.skyflow.vault.data; + +@FunctionalInterface +public interface RequestInterceptor { + void intercept(RequestContext context); +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java new file mode 100644 index 00000000..abeabdbe --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenGroupRedactions.java @@ -0,0 +1,39 @@ +package com.skyflow.vault.data; + +public class TokenGroupRedactions extends BaseDetokenizeData { + private final TokenGroupRedactionsBuilder builder; + + private TokenGroupRedactions(TokenGroupRedactionsBuilder builder) { + this.builder = builder; + } + public String getTokenGroupName() { + return this.builder.tokenGroupName; + } + + public String getRedaction() { + return this.builder.redaction; + } + + public static TokenGroupRedactionsBuilder builder() { + return new TokenGroupRedactionsBuilder(); + } + + public static final class TokenGroupRedactionsBuilder { + private String tokenGroupName; + private String redaction; + + public TokenGroupRedactionsBuilder tokenGroupName(String tokenGroupName) { + this.tokenGroupName = tokenGroupName; + return this; + } + + public TokenGroupRedactionsBuilder redaction(String redaction) { + this.redaction = redaction; + return this; + } + + public TokenGroupRedactions build() { + return new TokenGroupRedactions(this); + } + } +} \ No newline at end of file diff --git a/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java new file mode 100644 index 00000000..f955a731 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeOptions.java @@ -0,0 +1,38 @@ +package com.skyflow.vault.data; + +/** + * 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; + + protected TokenizeOptions(Builder builder) { + this.interceptor = builder.interceptor; + } + + public RequestInterceptor getInterceptor() { + return interceptor; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private RequestInterceptor interceptor; + + protected Builder() {} + + public Builder interceptor(RequestInterceptor interceptor) { + this.interceptor = interceptor; + return this; + } + + public TokenizeOptions build() { + return new TokenizeOptions(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/TokenizeSummary.java b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeSummary.java new file mode 100644 index 00000000..b9a692c0 --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/TokenizeSummary.java @@ -0,0 +1,39 @@ +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.Expose; + +public class TokenizeSummary { + @Expose(serialize = true) + private int totalTokens; + + @Expose(serialize = true) + private int totalTokenized; + + @Expose(serialize = true) + private int totalPartial; + + @Expose(serialize = true) + private int totalFailed; + + public TokenizeSummary() {} + + public TokenizeSummary(int totalTokens, int totalTokenized, int totalPartial, int totalFailed) { + this.totalTokens = totalTokens; + this.totalTokenized = totalTokenized; + this.totalPartial = totalPartial; + this.totalFailed = totalFailed; + } + + public int getTotalTokens() { return totalTokens; } + public int getTotalTokenized() { return totalTokenized; } + public int getTotalPartial() { return totalPartial; } + public int getTotalFailed() { return totalFailed; } + + @Override + public String toString() { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + return gson.toJson(this); + } +} diff --git a/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java b/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java new file mode 100644 index 00000000..dddaedaf --- /dev/null +++ b/flowvault/src/main/java/com/skyflow/vault/data/UpsertOptions.java @@ -0,0 +1,42 @@ +package com.skyflow.vault.data; + +import java.util.List; + +public class UpsertOptions { + private final UpsertOptionsBuilder builder; + + private UpsertOptions(UpsertOptionsBuilder builder) { + this.builder = builder; + } + + public static UpsertOptionsBuilder builder() { + return new UpsertOptionsBuilder(); + } + + public String getUpdateType() { + return this.builder.updateType; + } + + public List getUniqueColumns() { + return this.builder.uniqueColumns; + } + + public static final class UpsertOptionsBuilder { + private String updateType; + private List uniqueColumns; + + public UpsertOptionsBuilder updateType(String updateType) { + this.updateType = updateType; + return this; + } + + public UpsertOptionsBuilder uniqueColumns(List uniqueColumns) { + this.uniqueColumns = uniqueColumns; + return this; + } + + public UpsertOptions build() { + return new UpsertOptions(this); + } + } +} diff --git a/flowvault/src/main/resources/sdk.properties b/flowvault/src/main/resources/sdk.properties new file mode 100644 index 00000000..a33c65cd --- /dev/null +++ b/flowvault/src/main/resources/sdk.properties @@ -0,0 +1 @@ +sdk.version=${sdk.version} 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/ClientLifecycleScenarioTests.java b/flowvault/src/test/java/com/skyflow/ClientLifecycleScenarioTests.java new file mode 100644 index 00000000..0a39efb5 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/ClientLifecycleScenarioTests.java @@ -0,0 +1,212 @@ +package com.skyflow; + +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.controller.VaultController; +import org.junit.Assert; +import org.junit.Test; + +/** + * The four client lifecycle scenarios, mirrored by the ClientOperationsExample sample. + * + *

Each scenario runs twice: once through {@code SkyflowClientBuilder} and once through the built + * {@code Skyflow} client. The two go down different code paths — BaseSkyflow.updateVaultConfig calls + * the template directly and skips the builder's own override — and a bug that dropped the + * flowvault-specific fields on the client path only was found exactly this way. + * + *

Unlike the sample, these run inside the com.skyflow package, so they can assert against the + * controller's own config rather than only the stored copy. + */ +public class ClientLifecycleScenarioTests { + + private static final String VAULT_ID = "vault1"; + private static final String NOT_IN_CONFIG_LIST = "VaultId is missing from the config"; + + private static VaultConfig config(String clusterId) { + VaultConfig config = new VaultConfig(); + config.setVaultId(VAULT_ID); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + return config; + } + + /** An update carrying a new cluster, env and timeout. clusterId is resent because the incoming + * config is validated on its own before being merged. */ + private static VaultConfig update(String clusterId, Env env, Integer timeout) { + VaultConfig update = config(clusterId); + update.setEnv(env); + update.setTimeout(timeout); + return update; + } + + // ── A: add -> update -> delete -> vault() must fail ─────────────────────── + + @Test + public void testScenarioA_viaBuilder_addUpdateDeleteThenVaultFails() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config("cluster1")); + + // add + Assert.assertEquals("cluster1", builder.build().getVaultConfig(VAULT_ID).getClusterId()); + // update - no error + builder.updateVaultConfig(update("cluster2", Env.PROD, 30)); + Assert.assertEquals("cluster2", builder.build().getVaultConfig(VAULT_ID).getClusterId()); + Assert.assertEquals(Env.PROD, builder.build().getVaultConfig(VAULT_ID).getEnv()); + Assert.assertEquals(Integer.valueOf(30), builder.build().getVaultConfig(VAULT_ID).getTimeout()); + // delete + builder.removeVaultConfig(VAULT_ID); + Assert.assertNull(builder.build().getVaultConfig(VAULT_ID)); + + // vault() -> vault id not found + Skyflow client = builder.build(); + try { + client.vault(); + Assert.fail("vault() must fail once the vault is removed"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testScenarioA_viaClient_addUpdateDeleteThenVaultFails() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(config("cluster1")).build(); + + client.updateVaultConfig(update("cluster2", Env.PROD, 30)); + Assert.assertEquals("cluster2", client.getVaultConfig(VAULT_ID).getClusterId()); + Assert.assertEquals(Integer.valueOf(30), client.getVaultConfig(VAULT_ID).getTimeout()); + + client.removeVaultConfig(VAULT_ID); + Assert.assertNull(client.getVaultConfig(VAULT_ID)); + + try { + client.vault(); + Assert.fail("vault() must fail once the vault is removed"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + // ── B: add -> delete -> update must throw ───────────────────────────────── + + @Test + public void testScenarioB_viaBuilder_addDeleteThenUpdateThrows() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(config("cluster1")) + .removeVaultConfig(VAULT_ID); + + try { + builder.updateVaultConfig(update("cluster2", Env.PROD, 30)); + Assert.fail("updating a removed vault must throw, not silently re-create it"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + + // the failed update must not have resurrected the vault + Assert.assertNull(builder.build().getVaultConfig(VAULT_ID)); + } + + @Test + public void testScenarioB_viaClient_addDeleteThenUpdateThrows() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(config("cluster1")).build(); + client.removeVaultConfig(VAULT_ID); + + try { + client.updateVaultConfig(update("cluster2", Env.PROD, 30)); + Assert.fail("updating a removed vault must throw, not silently re-create it"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + + Assert.assertNull(client.getVaultConfig(VAULT_ID)); + try { + client.vault(); + Assert.fail("vault() must still fail after the rejected update"); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + // ── C: add -> update -> vault() carries the latest config ───────────────── + + @Test + public void testScenarioC_viaBuilder_vaultAfterUpdateHasLatest() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config("cluster1")); + + builder.updateVaultConfig(update("cluster2", Env.PROD, 15)); + VaultController vault = builder.build().vault(); + + Assert.assertEquals("cluster2", vault.getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, vault.getVaultConfig().getEnv()); + Assert.assertEquals(Integer.valueOf(15), vault.getVaultConfig().getTimeout()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", vault.currentVaultURL); + vault.updateExecutorInHTTP(); + Assert.assertEquals(15_000, vault.sharedHttpClient.callTimeoutMillis()); + } + + @Test + public void testScenarioC_viaClient_vaultAfterUpdateHasLatest() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(config("cluster1")).build(); + + client.updateVaultConfig(update("cluster2", Env.PROD, 15)); + VaultController vault = client.vault(); + + Assert.assertEquals("cluster2", vault.getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, vault.getVaultConfig().getEnv()); + Assert.assertEquals(Integer.valueOf(15), vault.getVaultConfig().getTimeout()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", vault.currentVaultURL); + vault.updateExecutorInHTTP(); + Assert.assertEquals(15_000, vault.sharedHttpClient.callTimeoutMillis()); + } + + // ── D: add -> vault() -> update -> vault() carries the latest config ────── + + @Test + public void testScenarioD_viaBuilder_heldControllerSeesTheUpdate() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config("cluster1")); + VaultController held = builder.build().vault(); + Assert.assertEquals("cluster1", held.getVaultConfig().getClusterId()); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", held.currentVaultURL); + + builder.updateVaultConfig(update("cluster2", Env.PROD, 45)); + VaultController after = builder.build().vault(); + + Assert.assertSame("the reference taken before the update must still be current", held, after); + Assert.assertEquals("cluster2", held.getVaultConfig().getClusterId()); + Assert.assertEquals(Integer.valueOf(45), held.getVaultConfig().getTimeout()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", held.currentVaultURL); + } + + @Test + public void testScenarioD_viaClient_heldControllerSeesTheUpdate() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(config("cluster1")).build(); + VaultController held = client.vault(); + Assert.assertEquals("cluster1", held.getVaultConfig().getClusterId()); + + client.updateVaultConfig(update("cluster2", Env.PROD, 45)); + + Assert.assertSame("the reference taken before the update must still be current", + held, client.vault()); + Assert.assertEquals("cluster2", held.getVaultConfig().getClusterId()); + Assert.assertEquals(Integer.valueOf(45), held.getVaultConfig().getTimeout()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", held.currentVaultURL); + held.updateExecutorInHTTP(); + Assert.assertEquals(45_000, held.sharedHttpClient.callTimeoutMillis()); + } + + // ── the vaultUrl variant of C/D, since it resolves differently to clusterId ── + + @Test + public void testScenarioD_viaClient_heldControllerSeesANewVaultUrl() throws SkyflowException { + VaultConfig initial = config("cluster1"); + initial.setVaultUrl("https://first.example.com"); + Skyflow client = Skyflow.builder().addVaultConfig(initial).build(); + VaultController held = client.vault(); + Assert.assertEquals("https://first.example.com", held.currentVaultURL); + + VaultConfig update = config("cluster1"); + update.setVaultUrl("https://second.example.com"); + client.updateVaultConfig(update); + + Assert.assertEquals("https://second.example.com", held.currentVaultURL); + } +} diff --git a/flowvault/src/test/java/com/skyflow/HttpConfigTests.java b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java new file mode 100644 index 00000000..3e0e4f7f --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/HttpConfigTests.java @@ -0,0 +1,685 @@ +package com.skyflow; + +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +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.util.List; + +/** + * Covers the HTTP timeout / retry configuration and its three-level resolution: + * vault-level override -> client-wide default -> SDK default. + */ +public class HttpConfigTests { + + // OkHttp's own defaults, applied when we leave a per-attempt timeout unset. + private static final int OKHTTP_DEFAULT_TIMEOUT_MILLIS = 10_000; + private static final int SDK_DEFAULT_CALL_TIMEOUT_MILLIS = 60_000; + + private static VaultConfig buildConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault1"); + config.setClusterId("cluster1"); + config.setEnv(Env.DEV); + return config; + } + + /** Builds the shared OkHttp client without needing credentials or a live token. */ + private static OkHttpClient httpClientOf(VaultClient client) { + client.updateExecutorInHTTP(); + return client.sharedHttpClient; + } + + private static int maxRetriesOf(SkyflowRetryInterceptor interceptor) { + return interceptor.getMaxRetries(); + } + + private static SkyflowRetryInterceptor retryInterceptorOf(OkHttpClient http) { + for (Interceptor interceptor : http.interceptors()) { + if (interceptor instanceof SkyflowRetryInterceptor) { + return (SkyflowRetryInterceptor) interceptor; + } + } + throw new AssertionError("No SkyflowRetryInterceptor installed on the HTTP client"); + } + + // ── SDK defaults (neither level configured) ─────────────────────────────── + + @Test + public void testDefaults_callTimeoutIs60Seconds() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, http.callTimeoutMillis()); + } + + @Test + public void testDefaults_retriesAreOff() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testDefaults_perAttemptTimeoutsLeftAtOkHttpDefaults() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.connectTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.readTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.writeTimeoutMillis()); + } + + // ── Client-wide values only ─────────────────────────────────────────────── + + @Test + public void testClientWideConfig_appliesWhenVaultLevelUnset() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(6_000, http.readTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + // ── Vault-level values only ─────────────────────────────────────────────── + + @Test + public void testVaultLevelConfig_appliesWhenClientWideUnset() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + OkHttpClient http = httpClientOf(new VaultClient(config, null)); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + // ── Precedence: both levels set ─────────────────────────────────────────── + + @Test + public void testPrecedence_vaultLevelBeatsClientWideForEverySetting() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_settingsResolveIndependently() throws SkyflowException { + // Vault overrides only the read timeout; everything else falls through to client-wide. + VaultConfig config = buildConfig(); + config.setReadTimeout(12); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, 5, 6, 7, 2, null, null); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_sdkDefaultUsedWhenBothLevelsNull() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + client.setCommonHttpConfig(null, null, null, null, null, null, null); + + OkHttpClient http = httpClientOf(client); + + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, http.callTimeoutMillis()); + Assert.assertEquals(0, maxRetriesOf(retryInterceptorOf(http))); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MILLIS, http.connectTimeoutMillis()); + } + + // ── Cache invalidation ──────────────────────────────────────────────────── + + @Test + public void testSetCommonHttpConfig_rebuildsHttpClientWithNewValues() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + OkHttpClient first = httpClientOf(client); + Assert.assertEquals(SDK_DEFAULT_CALL_TIMEOUT_MILLIS, first.callTimeoutMillis()); + + client.setCommonHttpConfig(30, null, null, null, 2, null, null); + OkHttpClient second = httpClientOf(client); + + Assert.assertNotSame(first, second); + Assert.assertEquals(30_000, second.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(second))); + } + + @Test + public void testUpdateExecutorInHTTP_reusesCachedClientWhenConfigUnchanged() throws SkyflowException { + VaultClient client = new VaultClient(buildConfig(), null); + + OkHttpClient first = httpClientOf(client); + OkHttpClient second = httpClientOf(client); + + Assert.assertSame(first, second); + } + + // ── Interceptor wiring ──────────────────────────────────────────────────── + + @Test + public void testInterceptors_retryIsOuterSoEachAttemptRereadsTheToken() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + 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 SkyflowRetryInterceptor); + Assert.assertFalse(interceptors.get(1) instanceof SkyflowRetryInterceptor); + } + + @Test + public void testConnectionPool_isConfigured() throws SkyflowException { + OkHttpClient http = httpClientOf(new VaultClient(buildConfig(), null)); + + Assert.assertNotNull(http.connectionPool()); + } + + // ── Boxed-Integer semantics ─────────────────────────────────────────────── + + @Test + public void testExplicitZeroTimeout_isAValueNotAnInheritSignal() throws SkyflowException { + // Only null means "inherit". An explicit 0 wins over the client-wide value, and OkHttp + // reads 0 as "no timeout" — so this disables the overall ceiling rather than restoring 60s. + VaultConfig config = buildConfig(); + config.setTimeout(0); + + VaultClient client = new VaultClient(config, null); + client.setCommonHttpConfig(30, null, null, null, null, null, null); + + Assert.assertEquals(0, httpClientOf(client).callTimeoutMillis()); + } + + @Test + public void testExplicitZeroMaxRetries_overridesClientWideRetries() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setMaxRetries(0); + + VaultClient client = new VaultClient(config, null); + 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 + public void testVaultConfig_httpSettingsDefaultToNull() { + VaultConfig config = new VaultConfig(); + + Assert.assertNull(config.getTimeout()); + Assert.assertNull(config.getConnectTimeout()); + Assert.assertNull(config.getReadTimeout()); + Assert.assertNull(config.getWriteTimeout()); + Assert.assertNull(config.getMaxRetries()); + } + + @Test + public void testVaultConfig_settersRoundTrip() { + VaultConfig config = new VaultConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + Assert.assertEquals(Integer.valueOf(45), config.getTimeout()); + Assert.assertEquals(Integer.valueOf(11), config.getConnectTimeout()); + Assert.assertEquals(Integer.valueOf(12), config.getReadTimeout()); + Assert.assertEquals(Integer.valueOf(13), config.getWriteTimeout()); + Assert.assertEquals(Integer.valueOf(4), config.getMaxRetries()); + } + + // ── Skyflow builder wiring ──────────────────────────────────────────────── + + @Test + public void testBuilder_httpConfigSetBeforeAddVaultConfigReachesController() throws SkyflowException { + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(buildConfig()) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(5_000, http.connectTimeoutMillis()); + Assert.assertEquals(6_000, http.readTimeoutMillis()); + Assert.assertEquals(7_000, http.writeTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_httpConfigSetAfterAddVaultConfigStillReachesController() throws SkyflowException { + // Order independence: propagateHttpConfig() must reach controllers already built. + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig()) + .timeout(30) + .maxRetries(2) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_vaultConfigOverridesBuilderValueEndToEnd() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder() + .timeout(30) + .addVaultConfig(config) + .build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testBuilder_httpConfigSurvivesUpdateVaultConfig() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .timeout(30) + .maxRetries(2) + .addVaultConfig(buildConfig()); + + VaultConfig updated = buildConfig(); + updated.setClusterId("cluster2"); + Skyflow client = builder.updateVaultConfig(updated).build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(30_000, http.callTimeoutMillis()); + Assert.assertEquals(2, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testBuilder_httpConfigAppliesToEveryVault() throws SkyflowException { + VaultConfig second = new VaultConfig(); + second.setVaultId("vault2"); + second.setClusterId("cluster2"); + second.setEnv(Env.DEV); + + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig()) + .addVaultConfig(second) + .timeout(30); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + Assert.assertEquals(30_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + Assert.assertEquals(30_000, + httpClientOf(builder.removeVaultConfig("vault1").build().vault()).callTimeoutMillis()); + } + + @Test + public void testBuilder_httpConfigMethodsAreFluent() { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder(); + + Assert.assertSame(builder, builder.timeout(30)); + Assert.assertSame(builder, builder.connectTimeout(5)); + Assert.assertSame(builder, builder.readTimeout(6)); + Assert.assertSame(builder, builder.writeTimeout(7)); + Assert.assertSame(builder, builder.maxRetries(2)); + } + + // ── Precedence through the public API: both levels set ──────────────────── + // Each test sets the same setting on Skyflow.builder() AND on VaultConfig, then asserts the + // VaultConfig value is what reaches the wire client. + + @Test + public void testPrecedence_endToEnd_vaultTimeoutBeatsClientLevelTimeout() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder().timeout(30).addVaultConfig(config).build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultConnectTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setConnectTimeout(11); + + Skyflow client = Skyflow.builder().connectTimeout(5).addVaultConfig(config).build(); + + Assert.assertEquals(11_000, httpClientOf(client.vault()).connectTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultReadTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setReadTimeout(12); + + Skyflow client = Skyflow.builder().readTimeout(6).addVaultConfig(config).build(); + + Assert.assertEquals(12_000, httpClientOf(client.vault()).readTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultWriteTimeoutBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setWriteTimeout(13); + + Skyflow client = Skyflow.builder().writeTimeout(7).addVaultConfig(config).build(); + + Assert.assertEquals(13_000, httpClientOf(client.vault()).writeTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultMaxRetriesBeatsClientLevel() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder().maxRetries(2).addVaultConfig(config).build(); + + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(httpClientOf(client.vault())))); + } + + @Test + public void testPrecedence_endToEnd_allFiveSetAtBothLevels() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(config) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(11_000, http.connectTimeoutMillis()); + Assert.assertEquals(12_000, http.readTimeoutMillis()); + Assert.assertEquals(13_000, http.writeTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_partialOverrideTakesEachLevelPerSetting() throws SkyflowException { + // Vault overrides timeout and maxRetries only; the rest come from the client level. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setMaxRetries(4); + + Skyflow client = Skyflow.builder() + .timeout(30) + .connectTimeout(5) + .readTimeout(6) + .writeTimeout(7) + .maxRetries(2) + .addVaultConfig(config) + .build(); + + OkHttpClient http = httpClientOf(client.vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); // vault + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); // vault + Assert.assertEquals(5_000, http.connectTimeoutMillis()); // client + Assert.assertEquals(6_000, http.readTimeoutMillis()); // client + Assert.assertEquals(7_000, http.writeTimeoutMillis()); // client + } + + @Test + public void testPrecedence_endToEnd_holdsWhenClientLevelIsSetAfterAddVaultConfig() throws SkyflowException { + // Builder call order must not change who wins. + VaultConfig config = buildConfig(); + config.setTimeout(45); + + Skyflow client = Skyflow.builder().addVaultConfig(config).timeout(30).build(); + + Assert.assertEquals(45_000, httpClientOf(client.vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_appliesPerVaultNotGlobally() throws SkyflowException { + // vault1 overrides the timeout; vault2 leaves it unset and inherits the client-level value. + VaultConfig overriding = buildConfig(); + overriding.setTimeout(45); + + VaultConfig inheriting = new VaultConfig(); + inheriting.setVaultId("vault2"); + inheriting.setClusterId("cluster2"); + inheriting.setEnv(Env.DEV); + + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .timeout(30) + .addVaultConfig(overriding) + .addVaultConfig(inheriting); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + Assert.assertEquals(45_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + Assert.assertEquals(30_000, + httpClientOf(builder.removeVaultConfig("vault1").build().vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_vaultStillWinsAfterUpdateVaultConfig() throws SkyflowException { + VaultConfig config = buildConfig(); + config.setTimeout(45); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setTimeout(90); + + Assert.assertEquals(90_000, + httpClientOf(builder.updateVaultConfig(update).build().vault()).callTimeoutMillis()); + } + + @Test + public void testPrecedence_endToEnd_updateWithoutHttpSettingsKeepsTheExistingOnes() throws SkyflowException { + // A null on the incoming config means "leave as is", matching how the base class merges. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setMaxRetries(4); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setClusterId("cluster2"); + + OkHttpClient http = httpClientOf(builder.updateVaultConfig(update).build().vault()); + + Assert.assertEquals(45_000, http.callTimeoutMillis()); + Assert.assertEquals(4, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_updateVaultConfigCarriesAllFiveHttpSettings() throws SkyflowException { + // Regression: BaseSkyflow.mergeVaultConfig() carries only env/clusterId/credentials, so + // without SkyflowClientBuilder.carryHttpOverrides() every value below is silently dropped + // and the vault keeps running on its original settings. + VaultConfig config = buildConfig(); + config.setTimeout(45); + config.setConnectTimeout(11); + config.setReadTimeout(12); + config.setWriteTimeout(13); + config.setMaxRetries(4); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultConfig update = buildConfig(); + update.setTimeout(90); + update.setConnectTimeout(21); + update.setReadTimeout(22); + update.setWriteTimeout(23); + update.setMaxRetries(8); + + OkHttpClient http = httpClientOf(builder.updateVaultConfig(update).build().vault()); + + Assert.assertEquals(90_000, http.callTimeoutMillis()); + Assert.assertEquals(21_000, http.connectTimeoutMillis()); + Assert.assertEquals(22_000, http.readTimeoutMillis()); + Assert.assertEquals(23_000, http.writeTimeoutMillis()); + Assert.assertEquals(8, maxRetriesOf(retryInterceptorOf(http))); + } + + @Test + public void testPrecedence_endToEnd_updateCanIntroduceAVaultOverride() throws SkyflowException { + // Vault starts with no override (inherits 30), then an update introduces one. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().timeout(30).addVaultConfig(buildConfig()); + Assert.assertEquals(30_000, httpClientOf(builder.build().vault()).callTimeoutMillis()); + + VaultConfig update = buildConfig(); + update.setTimeout(45); + + Assert.assertEquals(45_000, + httpClientOf(builder.updateVaultConfig(update).build().vault()).callTimeoutMillis()); + } + + @Test + public void testController_isAVaultClientSoItInheritsTheHttpConfig() throws SkyflowException { + Skyflow client = Skyflow.builder().timeout(30).addVaultConfig(buildConfig()).build(); + + VaultController controller = client.vault(); + + Assert.assertEquals(30_000, httpClientOf(controller).callTimeoutMillis()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/SkyflowTests.java b/flowvault/src/test/java/com/skyflow/SkyflowTests.java new file mode 100644 index 00000000..7986e68a --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/SkyflowTests.java @@ -0,0 +1,519 @@ +package com.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.controller.VaultController; +import org.junit.Assert; +import org.junit.Test; + +public class SkyflowTests { + private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + + private static VaultConfig buildConfig(String vaultId, String clusterId) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + return config; + } + + // ── addVaultConfig ──────────────────────────────────────────────────────── + + @Test + public void testAddVaultConfig_success() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertEquals("vault1", client.getVaultConfig("vault1").getVaultId()); + } + + @Test + public void testAddVaultConfig_duplicateVaultIdThrows() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + try { + builder.addVaultConfig(buildConfig("vault1", "cluster2")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testAddVaultConfig_invalidConfigThrows() { + VaultConfig config = new VaultConfig(); + // no vaultId set + try { + Skyflow.builder().addVaultConfig(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── updateVaultConfig: flowvault-specific fields ───────────────────────── + // BaseSkyflow.mergeVaultConfig() only carries env/clusterId/credentials, so vaultUrl needs + // SkyflowClientBuilder.carryVaultOverrides() to survive an update. + + @Test + public void testUpdateVaultConfig_changesVaultURL() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + Assert.assertEquals("https://first.example.com", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://second.example.com"); + + Assert.assertEquals("https://second.example.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_storesTheNewVaultURLOnTheConfig() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://second.example.com"); + + Assert.assertEquals("https://second.example.com", + builder.updateVaultConfig(update).build().getVaultConfig("vault1").getVaultUrl()); + } + + @Test + public void testUpdateVaultConfig_omittingVaultURLKeepsTheExistingOne() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + // No vaultUrl on the update: null means "leave as is", as elsewhere in the merge. + Skyflow client = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build(); + + Assert.assertEquals("https://first.example.com", client.vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_canIntroduceAVaultURLWhereClusterIdWasUsed() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://explicit.example.com"); + + Assert.assertEquals("https://explicit.example.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_clusterIdChangeStillRebuildsTheURL() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + Skyflow client = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build(); + + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + + // ── updateVaultConfig ───────────────────────────────────────────────────── + + @Test + public void testUpdateVaultConfig_success() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + VaultConfig update = new VaultConfig(); + update.setVaultId("vault1"); + update.setClusterId("cluster2"); + update.setEnv(Env.PROD); + client.updateVaultConfig(update); + + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals(Env.PROD, client.getVaultConfig("vault1").getEnv()); + } + + @Test + public void testUpdateVaultConfig_nonExistentVaultIdThrows() { + try { + Skyflow.builder().updateVaultConfig(buildConfig("vault-unknown", "cluster1")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── removeVaultConfig ───────────────────────────────────────────────────── + + @Test + public void testRemoveVaultConfig_success() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + client.removeVaultConfig("vault1"); + Assert.assertNull(client.getVaultConfig("vault1")); + } + + @Test + public void testRemoveVaultConfig_nonExistentVaultIdThrows() { + try { + Skyflow.builder().removeVaultConfig("vault-unknown"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── updateVaultConfig validates the incoming config, not the merged result ── + + @Test + public void testUpdateVaultConfig_partialUpdateWithoutClusterIdOrVaultUrlIsRejected() throws SkyflowException { + // mergeVaultConfig only copies non-null fields across, which implies "send just what you + // want to change". But updateVaultConfigTemplate validates the INCOMING config first, and + // validateVaultConfiguration requires clusterId or vaultUrl - so a partial update is + // rejected even though the merge would have preserved the existing values. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig partial = new VaultConfig(); + partial.setVaultId("vault1"); + partial.setTimeout(30); + + try { + builder.updateVaultConfig(partial); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains("clusterId")); + } + } + + @Test + public void testUpdateVaultConfig_rejectedPartialUpdateChangesNothing() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig partial = new VaultConfig(); + partial.setVaultId("vault1"); + partial.setTimeout(30); + try { + builder.updateVaultConfig(partial); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException expected) { + // asserted above + } + + Skyflow client = builder.build(); + Assert.assertEquals("cluster1", client.getVaultConfig("vault1").getClusterId()); + Assert.assertNull("the rejected timeout must not have been applied", + client.getVaultConfig("vault1").getTimeout()); + } + + @Test + public void testUpdateVaultConfig_partialUpdateIsAcceptedWhenClusterIdIsRepeated() throws SkyflowException { + // The workaround: resend clusterId even when it is not changing. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = new VaultConfig(); + update.setVaultId("vault1"); + update.setClusterId("cluster1"); + update.setTimeout(30); + + Skyflow client = builder.updateVaultConfig(update).build(); + + Assert.assertEquals(Integer.valueOf(30), client.getVaultConfig("vault1").getTimeout()); + Assert.assertEquals("cluster1", client.getVaultConfig("vault1").getClusterId()); + } + + @Test + public void testUpdateVaultConfig_vaultUrlAloneSatisfiesTheRequirement() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = new VaultConfig(); + update.setVaultId("vault1"); + update.setVaultUrl("https://custom.example.com"); + update.setTimeout(30); + + Skyflow client = builder.updateVaultConfig(update).build(); + + Assert.assertEquals("https://custom.example.com", client.vault().currentVaultURL); + Assert.assertEquals(Integer.valueOf(30), client.getVaultConfig("vault1").getTimeout()); + } + + // ── Client management lifecycles ───────────────────────────────────────── + // Whole add/update/remove sequences, asserting both the stored config and the controller + // behind vault() stay in step at every stage. + + private static final String NOT_IN_CONFIG_LIST = "VaultId is missing from the config"; + + @Test + public void testLifecycle_addThenRemove() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + Skyflow afterAdd = builder.build(); + Assert.assertEquals("cluster1", afterAdd.getVaultConfig("vault1").getClusterId()); + Assert.assertNotNull(afterAdd.vault()); + + Skyflow afterRemove = builder.removeVaultConfig("vault1").build(); + + Assert.assertNull(afterRemove.getVaultConfig("vault1")); + try { + afterRemove.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addThenRemoveThenReAddSameVaultId() throws SkyflowException { + // Removing must clear the id, otherwise re-adding would trip the duplicate check. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1") + .addVaultConfig(buildConfig("vault1", "cluster2")); + + Skyflow client = builder.build(); + + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + + @Test + public void testLifecycle_addRemoveThenUpdateFails() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1"); + + try { + builder.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addRemoveThenFailedUpdateLeavesNoConfigBehind() throws SkyflowException { + // A rejected update must not resurrect the removed vault. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .removeVaultConfig("vault1"); + try { + builder.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException expected) { + // asserted in testLifecycle_addRemoveThenUpdateFails + } + + Skyflow client = builder.build(); + + Assert.assertNull(client.getVaultConfig("vault1")); + try { + client.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addUpdateThenRemove() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + Skyflow afterUpdate = builder.updateVaultConfig(update).build(); + + Assert.assertEquals("cluster2", afterUpdate.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals(Env.PROD, afterUpdate.getVaultConfig("vault1").getEnv()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", afterUpdate.vault().currentVaultURL); + + Skyflow afterRemove = builder.removeVaultConfig("vault1").build(); + + Assert.assertNull(afterRemove.getVaultConfig("vault1")); + try { + afterRemove.vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_addUpdateRemoveThenRemoveAgainFails() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .updateVaultConfig(buildConfig("vault1", "cluster2")) + .removeVaultConfig("vault1"); + + try { + builder.removeVaultConfig("vault1"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + @Test + public void testLifecycle_removingOneVaultLeavesTheOtherIntact() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .removeVaultConfig("vault1") + .build(); + + Assert.assertNull(client.getVaultConfig("vault1")); + Assert.assertEquals("cluster2", client.getVaultConfig("vault2").getClusterId()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.dev", client.vault().currentVaultURL); + } + + @Test + public void testLifecycle_onTheBuiltClientRatherThanTheBuilder() throws SkyflowException { + // Skyflow exposes the same add/update/remove surface as the builder; exercise that path too. + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + client.updateVaultConfig(buildConfig("vault1", "cluster2")); + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + + client.removeVaultConfig("vault1"); + Assert.assertNull(client.getVaultConfig("vault1")); + + try { + client.updateVaultConfig(buildConfig("vault1", "cluster3")); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertTrue(e.getMessage().contains(NOT_IN_CONFIG_LIST)); + } + } + + // ── addSkyflowCredentials ───────────────────────────────────────────────── + + @Test + public void testAddSkyflowCredentials_success() throws SkyflowException { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addSkyflowCredentials(credentials) + .build(); + Assert.assertNotNull(client); + } + + @Test + public void testAddSkyflowCredentials_invalidCredentialsThrows() { + Credentials credentials = new Credentials(); + credentials.setApiKey("not-a-valid-api-key"); + try { + Skyflow.builder().addSkyflowCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── setLogLevel / getLogLevel ───────────────────────────────────────────── + + @Test + public void testSetLogLevel_updatesLogLevel() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .setLogLevel(LogLevel.DEBUG) + .build(); + Assert.assertEquals(LogLevel.DEBUG, client.getLogLevel()); + } + + @Test + public void testGetLogLevel_defaultsToError() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertEquals(LogLevel.ERROR, client.getLogLevel()); + } + + // ── vault() ─────────────────────────────────────────────────────────────── + + @Test + public void testVault_returnsVaultControllerWhenConfigured() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + VaultController controller = client.vault(); + Assert.assertNotNull(controller); + } + + @Test + public void testVault_throwsWhenNoConfigExists() { + try { + Skyflow.builder().build().vault(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── vault(vaultId) ──────────────────────────────────────────────────────── + + @Test + public void testVaultById_returnsTheControllerForTheConfiguredId() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertSame(client.vault(), client.vault("vault1")); + } + + @Test + public void testVaultById_selectsTheMatchingVaultAmongSeveral() throws SkyflowException { + VaultConfig first = buildConfig("vault1", "cluster1"); + first.setVaultUrl("https://first.example.com"); + VaultConfig second = buildConfig("vault2", "cluster2"); + second.setVaultUrl("https://second.example.com"); + Skyflow client = Skyflow.builder().addVaultConfig(first).addVaultConfig(second).build(); + + Assert.assertEquals("https://first.example.com", client.vault("vault1").currentVaultURL); + Assert.assertEquals("https://second.example.com", client.vault("vault2").currentVaultURL); + } + + @Test + public void testVaultById_nullIdResolvesToTheFirstConfiguredVault() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + Assert.assertSame(client.vault(), client.vault(null)); + } + + @Test + public void testVaultById_throwsForUnknownVaultId() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + try { + client.vault("vault-unknown"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testVaultById_throwsWhenNoConfigExists() { + try { + Skyflow.builder().build().vault("vault1"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testVaultById_removedVaultThrowsWhileOthersStillResolve() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .build(); + client.removeVaultConfig("vault1"); + + Assert.assertNotNull(client.vault("vault2")); + try { + client.vault("vault1"); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── getVaultConfig ──────────────────────────────────────────────────────── + + @Test + public void testGetVaultConfig_returnsNullForUnknownVaultId() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + Assert.assertNull(client.getVaultConfig("vault-unknown")); + } +} diff --git a/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java b/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java new file mode 100644 index 00000000..68c58fe4 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/UpdatePropagationTests.java @@ -0,0 +1,325 @@ +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.vault.controller.VaultController; +import org.junit.Assert; +import org.junit.Test; + +/** + * Verifies that an update reaches every place the value is actually consumed — the stored config, + * the controller holding it, and everything derived from it (vault URL, HTTP client, bearer token) + * — rather than only the copy in vaultConfigMap. + */ +public class UpdatePropagationTests { + + private static VaultConfig buildConfig(String vaultId, String clusterId) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + return config; + } + + private static Credentials tokenCredentials(String token) { + Credentials credentials = new Credentials(); + credentials.setToken(token); + return credentials; + } + + // ── updateVaultConfig reaches the controller, not just the stored config ── + + @Test + public void testUpdateVaultConfig_storedConfigAndControllerConfigAgree() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + Skyflow client = builder.updateVaultConfig(update).build(); + + // The copy the user can read back... + Assert.assertEquals("cluster2", client.getVaultConfig("vault1").getClusterId()); + Assert.assertEquals(Env.PROD, client.getVaultConfig("vault1").getEnv()); + // ...and the copy the controller actually builds requests from. + Assert.assertEquals("cluster2", client.vault().getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, client.vault().getVaultConfig().getEnv()); + } + + @Test + public void testUpdateVaultConfig_reconfiguresTheControllerInPlace() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController before = builder.build().vault(); + + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + + Assert.assertSame(before, after); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesTheNewConfig() throws SkyflowException { + // Replacing the controller instead of reconfiguring it would leave this reference talking + // to the old vault, with no error to say so. + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController held = builder.build().vault(); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", held.currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + builder.updateVaultConfig(update); + + Assert.assertEquals("cluster2", held.getVaultConfig().getClusterId()); + Assert.assertEquals(Env.PROD, held.getVaultConfig().getEnv()); + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", held.currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesNewCredentials() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + VaultController held = builder.build().vault(); + held.setBearerToken(); + Assert.assertEquals("first-token", held.token); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setCredentials(tokenCredentials("second-token")); + builder.updateVaultConfig(update); + held.setBearerToken(); + + Assert.assertEquals("second-token", held.token); + } + + @Test + public void testUpdateVaultConfig_handleHeldAcrossTheUpdateSeesNewHttpSettings() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + VaultController held = builder.build().vault(); + held.updateExecutorInHTTP(); + Assert.assertEquals(60_000, held.sharedHttpClient.callTimeoutMillis()); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setTimeout(45); + builder.updateVaultConfig(update); + held.updateExecutorInHTTP(); + + Assert.assertEquals(45_000, held.sharedHttpClient.callTimeoutMillis()); + } + + @Test + public void testUpdateVaultConfig_derivedVaultURLIsRebuilt() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", builder.build().vault().currentVaultURL); + + VaultConfig update = buildConfig("vault1", "cluster2"); + update.setEnv(Env.PROD); + + Assert.assertEquals("https://cluster2.skyvault.skyflowapis.com", + builder.updateVaultConfig(update).build().vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfig_newVaultLevelCredentialsReachTheToken() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + VaultController before = builder.build().vault(); + before.setBearerToken(); + Assert.assertEquals("first-token", before.token); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setCredentials(tokenCredentials("second-token")); + VaultController after = builder.updateVaultConfig(update).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("second-token", after.token); + } + + @Test + public void testUpdateVaultConfig_omittingCredentialsKeepsTheExistingOnes() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("first-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(config); + + // No credentials on the update: null means "leave as is". + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("first-token", after.token); + } + + @Test + public void testUpdateVaultConfig_stillHasTheClientWideCredentialsAfterwards() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")); + + // Reconfiguring must not clear the client-wide credentials the controller already holds. + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("common-token", after.token); + } + + @Test + public void testUpdateVaultConfig_vaultLevelCredentialsStillBeatClientWide() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("vault-token")); + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(config); + + VaultController after = builder.updateVaultConfig(buildConfig("vault1", "cluster2")).build().vault(); + after.setBearerToken(); + + Assert.assertEquals("vault-token", after.token); + } + + // ── the SAME must hold via the built client, not just the builder ───────── + // BaseSkyflow.updateVaultConfig bypasses the builder's override, so both entry points need + // covering. A sample calling client.updateVaultConfig(...) is what exposed this gap. + + @Test + public void testUpdateVaultConfigOnClient_carriesHttpSettings() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setTimeout(30); + update.setMaxRetries(4); + client.updateVaultConfig(update); + + Assert.assertEquals(Integer.valueOf(30), client.getVaultConfig("vault1").getTimeout()); + Assert.assertEquals(Integer.valueOf(4), client.getVaultConfig("vault1").getMaxRetries()); + } + + @Test + public void testUpdateVaultConfigOnClient_httpSettingsReachTheHttpClient() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setTimeout(30); + client.updateVaultConfig(update); + + VaultController vault = client.vault(); + vault.updateExecutorInHTTP(); + Assert.assertEquals(30_000, vault.sharedHttpClient.callTimeoutMillis()); + } + + @Test + public void testUpdateVaultConfigOnClient_carriesVaultUrl() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setVaultUrl("https://first.example.com"); + Skyflow client = Skyflow.builder().addVaultConfig(config).build(); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setVaultUrl("https://second.example.com"); + client.updateVaultConfig(update); + + Assert.assertEquals("https://second.example.com", client.getVaultConfig("vault1").getVaultUrl()); + Assert.assertEquals("https://second.example.com", client.vault().currentVaultURL); + } + + @Test + public void testUpdateVaultConfigOnClient_retryDelaysAreCarried() throws SkyflowException { + Skyflow client = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")).build(); + + VaultConfig update = buildConfig("vault1", "cluster1"); + update.setInitialRetryDelayMillis(250L); + update.setMaxRetryDelayMillis(4000L); + client.updateVaultConfig(update); + + Assert.assertEquals(Long.valueOf(250L), client.getVaultConfig("vault1").getInitialRetryDelayMillis()); + Assert.assertEquals(Long.valueOf(4000L), client.getVaultConfig("vault1").getMaxRetryDelayMillis()); + } + + // ── Credentials updates reach every controller ─────────────────────────── + + @Test + public void testAddSkyflowCredentials_afterVaultExists_reachesTheController() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(buildConfig("vault1", "cluster1")); + + VaultController controller = builder.addSkyflowCredentials(tokenCredentials("common-token")).build().vault(); + controller.setBearerToken(); + + Assert.assertEquals("common-token", controller.token); + } + + @Test + public void testAddSkyflowCredentials_beforeVaultExists_reachesTheNewController() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("common-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + + VaultController controller = client.vault(); + controller.setBearerToken(); + + Assert.assertEquals("common-token", controller.token); + } + + @Test + public void testAddSkyflowCredentials_reachesEveryVaultNotJustTheFirst() throws SkyflowException { + Skyflow.SkyflowClientBuilder builder = Skyflow.builder() + .addVaultConfig(buildConfig("vault1", "cluster1")) + .addVaultConfig(buildConfig("vault2", "cluster2")) + .addSkyflowCredentials(tokenCredentials("common-token")); + + VaultController first = builder.build().vault(); + first.setBearerToken(); + Assert.assertEquals("common-token", first.token); + + // vault() resolves the first entry, so drop vault1 to reach the second controller. + VaultController second = builder.removeVaultConfig("vault1").build().vault(); + second.setBearerToken(); + Assert.assertEquals("common-token", second.token); + } + + @Test + public void testUpdateSkyflowCredentials_invalidatesTheCachedToken() throws SkyflowException { + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("first-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + + VaultController controller = client.vault(); + controller.setBearerToken(); + Assert.assertEquals("first-token", controller.token); + + client.updateSkyflowCredentials(tokenCredentials("second-token")); + controller.setBearerToken(); + + Assert.assertEquals("A cached token must not survive a credentials change", + "second-token", controller.token); + } + + @Test + public void testUpdateSkyflowCredentials_doesNotOverrideVaultLevelCredentials() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1"); + config.setCredentials(tokenCredentials("vault-token")); + Skyflow client = Skyflow.builder().addVaultConfig(config).build(); + + client.updateSkyflowCredentials(tokenCredentials("common-token")); + VaultController controller = client.vault(); + controller.setBearerToken(); + + Assert.assertEquals("vault-token", controller.token); + } + + @Test + public void testUpdateSkyflowCredentials_appliesToTheSameControllerInstance() throws SkyflowException { + // Unlike updateVaultConfig, a credentials change must not swap the controller out — it + // reconfigures the existing one, so a handle the caller already holds stays valid. + Skyflow client = Skyflow.builder() + .addSkyflowCredentials(tokenCredentials("first-token")) + .addVaultConfig(buildConfig("vault1", "cluster1")) + .build(); + VaultController held = client.vault(); + + client.updateSkyflowCredentials(tokenCredentials("second-token")); + + Assert.assertSame(held, client.vault()); + held.setBearerToken(); + Assert.assertEquals("second-token", held.token); + } +} diff --git a/flowvault/src/test/java/com/skyflow/VaultClientTests.java b/flowvault/src/test/java/com/skyflow/VaultClientTests.java new file mode 100644 index 00000000..68f026fb --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/VaultClientTests.java @@ -0,0 +1,130 @@ +package com.skyflow; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +import org.junit.Assert; +import org.junit.Test; + +public class VaultClientTests { + + private static VaultConfig buildConfig(String vaultId, String clusterId, Credentials credentials) { + VaultConfig config = new VaultConfig(); + config.setVaultId(vaultId); + config.setClusterId(clusterId); + config.setEnv(Env.DEV); + if (credentials != null) { + config.setCredentials(credentials); + } + return config; + } + + // ── updateVaultUrl priority order ──────────────────────────────────────── + + @Test + public void testUpdateVaultURL_usesExplicitVaultURLOverClusterId() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1", null); + config.setVaultUrl("https://custom.example.com"); + + VaultClient client = new VaultClient(config, null); + + Assert.assertEquals("https://custom.example.com", client.currentVaultURL); + } + + @Test + public void testUpdateVaultURL_constructsFromClusterIdWhenNoVaultURL() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1", null); + + VaultClient client = new VaultClient(config, null); + + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", client.currentVaultURL); + } + + // ── setBearerToken ──────────────────────────────────────────────────────── + + @Test + public void testSetBearerToken_usesApiKeyDirectly() throws SkyflowException { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + VaultConfig config = buildConfig("vault1", "cluster1", creds); + + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + + Assert.assertEquals("sky-ab123-abcd1234cdef1234abcd4321cdef4321", client.token); + } + + @Test + public void testSetBearerToken_reusesNonExpiredToken() throws SkyflowException { + Credentials creds = new Credentials(); + creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); + VaultConfig config = buildConfig("vault1", "cluster1", creds); + + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + String firstToken = client.token; + client.setBearerToken(); + + Assert.assertEquals(firstToken, client.token); + } + + @Test + public void testSetBearerToken_invalidCredentialsThrows() { + Credentials creds = new Credentials(); + creds.setApiKey("not-a-valid-api-key"); + VaultConfig config = buildConfig("vault1", "cluster1", creds); + + try { + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + Assert.fail("Should have thrown an exception"); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── getRecordsApi ───────────────────────────────────────────────────────── + + @Test + public void testGetRecordsApi_availableAfterSetBearerToken() throws SkyflowException { + Credentials creds = new Credentials(); + creds.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + VaultConfig config = buildConfig("vault1", "cluster1", creds); + + VaultClient client = new VaultClient(config, null); + client.setBearerToken(); + + Assert.assertNotNull(client.getRecordsApi()); + } + + // ── setCommonCredentials ────────────────────────────────────────────────── + + @Test + public void testSetCommonCredentials_vaultSpecificCredentialsTakePriority() throws SkyflowException { + Credentials vaultCreds = new Credentials(); + vaultCreds.setToken("vault-specific-token"); + VaultConfig config = buildConfig("vault1", "cluster1", vaultCreds); + + VaultClient client = new VaultClient(config, null); + Credentials commonCreds = new Credentials(); + commonCreds.setToken("common-token"); + client.setCommonCredentials(commonCreds); + client.setBearerToken(); + + Assert.assertEquals("vault-specific-token", client.token); + } + + @Test + public void testSetCommonCredentials_usedWhenNoVaultSpecificCredentials() throws SkyflowException { + VaultConfig config = buildConfig("vault1", "cluster1", null); + + VaultClient client = new VaultClient(config, null); + Credentials commonCreds = new Credentials(); + commonCreds.setToken("common-token"); + client.setCommonCredentials(commonCreds); + client.setBearerToken(); + + Assert.assertEquals("common-token", client.token); + } +} diff --git a/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java b/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java new file mode 100644 index 00000000..065848a1 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/config/VaultConfigTests.java @@ -0,0 +1,94 @@ +package com.skyflow.config; + +import com.skyflow.enums.Env; +import org.junit.Assert; +import org.junit.Test; + +public class VaultConfigTests { + + @Test + public void testDefaultEnvIsProd() { + VaultConfig config = new VaultConfig(); + Assert.assertEquals(Env.PROD, config.getEnv()); + } + + @Test + public void testSettingNullEnvFallsBackToProd() { + VaultConfig config = new VaultConfig(); + config.setEnv(null); + Assert.assertEquals(Env.PROD, config.getEnv()); + } + + @Test + public void testVaultIdGetterSetter() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + Assert.assertEquals("vault123", config.getVaultId()); + } + + @Test + public void testClusterIdGetterSetter() { + VaultConfig config = new VaultConfig(); + config.setClusterId("cluster123"); + Assert.assertEquals("cluster123", config.getClusterId()); + } + + @Test + public void testEnvGetterSetter() { + VaultConfig config = new VaultConfig(); + config.setEnv(Env.SANDBOX); + Assert.assertEquals(Env.SANDBOX, config.getEnv()); + } + + @Test + public void testCredentialsGetterSetter() { + VaultConfig config = new VaultConfig(); + Credentials credentials = new Credentials(); + credentials.setToken("token1"); + config.setCredentials(credentials); + Assert.assertEquals(credentials, config.getCredentials()); + } + + @Test + public void testVaultURLDefaultsToNull() { + VaultConfig config = new VaultConfig(); + Assert.assertNull(config.getVaultUrl()); + } + + @Test + public void testVaultURLGetterSetter() { + VaultConfig config = new VaultConfig(); + config.setVaultUrl("https://myvault.example.com"); + Assert.assertEquals("https://myvault.example.com", config.getVaultUrl()); + } + + @Test + public void testClone_copiesFieldsAndDeepCopiesCredentials() throws CloneNotSupportedException { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster123"); + config.setEnv(Env.DEV); + Credentials credentials = new Credentials(); + credentials.setToken("token1"); + config.setCredentials(credentials); + + VaultConfig cloned = (VaultConfig) config.clone(); + + Assert.assertEquals(config.getVaultId(), cloned.getVaultId()); + Assert.assertEquals(config.getClusterId(), cloned.getClusterId()); + Assert.assertEquals(config.getEnv(), cloned.getEnv()); + Assert.assertNotSame(config.getCredentials(), cloned.getCredentials()); + Assert.assertEquals(config.getCredentials().getToken(), cloned.getCredentials().getToken()); + } + + @Test + public void testClone_withNullCredentials() throws CloneNotSupportedException { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster123"); + + VaultConfig cloned = (VaultConfig) config.clone(); + + Assert.assertNull(cloned.getCredentials()); + } +} 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 new file mode 100644 index 00000000..42b3dac9 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/RequestFidelityTests.java @@ -0,0 +1,908 @@ +package com.skyflow.utils; + +import com.skyflow.config.VaultConfig; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1InsertResponse; +import com.skyflow.generated.rest.types.V1RecordResponseObject; +import com.skyflow.generated.rest.types.V1TokenGroupRedactions; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkDeleteTokensResponse; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkDetokenizeResponse; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertResponse; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.BulkTokenizeRequest; +import com.skyflow.vault.data.InsertRequestRecord; +import com.skyflow.vault.data.TokenGroupRedactions; +import com.skyflow.vault.data.UpsertOptions; +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; + +/** + * Request-fidelity tests: every value an SDK user sets on a flowvault request object must reach + * the outgoing generated REST request object faithfully — same value, same field, same order — + * including across batching. + * + * These tests deliberately assert on the real mapping code in {@link Utils} rather than on any + * hand-rolled copy of it, and use {@code assertSame} where the SDK should be passing the user's + * own object through untouched (maps, lists, arbitrary tokenize values). + */ +public class RequestFidelityTests { + + private static final String VAULT_ID = "vault123"; + + // Values that are easy to mangle: non-ASCII, embedded spaces, punctuation. + private static final String NON_ASCII_NAME = "日本語 テスト Ω"; + private static final String SPACED_TABLE = "my table name"; + private static final String NON_ASCII_TABLE = "表_日本語"; + + private static VaultConfig vaultConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId(VAULT_ID); + return config; + } + + private static ArrayList recordList(InsertRequestRecord... records) { + return new ArrayList<>(Arrays.asList(records)); + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk insert — field fidelity + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkInsert_everyRecordFieldReachesWire() { + Map data = new LinkedHashMap<>(); + data.put("name", "john"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tok-abc"); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Arrays.asList("email", "phone")) + .build(); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertEquals("cards", body.getTableName().get()); + V1InsertRecordData wire = body.getRecords().get().get(0); + Assert.assertEquals("cards", wire.getTableName().get()); + // The user's own map instances must be handed to the wire object untouched. + Assert.assertSame(data, wire.getData().get()); + Assert.assertSame(tokens, wire.getTokens().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, wire.getUpsert().get().getUpdateType().get()); + Assert.assertEquals(Arrays.asList("email", "phone"), wire.getUpsert().get().getUniqueColumns().get()); + } + + @Test + public void testBulkInsert_nonAsciiAndSpacedValues_userValueReachesWire() { + Map data = new LinkedHashMap<>(); + data.put("name", NON_ASCII_NAME); + data.put("street address", "12 東京都 千代田区"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tök-ábc 123"); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName(NON_ASCII_TABLE) + .data(data) + .tokens(tokens) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName(SPACED_TABLE) + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals(SPACED_TABLE, body.getTableName().get()); + V1InsertRecordData wire = body.getRecords().get().get(0); + Assert.assertEquals(NON_ASCII_TABLE, wire.getTableName().get()); + Assert.assertEquals(NON_ASCII_NAME, wire.getData().get().get("name")); + Assert.assertEquals("12 東京都 千代田区", wire.getData().get().get("street address")); + Assert.assertEquals("tök-ábc 123", wire.getTokens().get().get("name")); + } + + @Test + public void testBulkInsert_nonStringDataValues_userValueReachesWire() { + Map nested = new LinkedHashMap<>(); + nested.put("city", "Paris"); + nested.put("zip", 75001); + List list = Arrays.asList(1, "two", true); + + Map data = new LinkedHashMap<>(); + data.put("age", 42); + data.put("balance", 1234.56d); + data.put("longValue", 9007199254740993L); + data.put("active", Boolean.TRUE); + data.put("address", nested); + data.put("tags", list); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + Map wireData = body.getRecords().get().get(0).getData().get(); + + Assert.assertSame(data, wireData); + // Object identity/type of every value survives — no toString()-ing, no boxing changes. + Assert.assertEquals(Integer.valueOf(42), wireData.get("age")); + Assert.assertEquals(Double.valueOf(1234.56d), wireData.get("balance")); + Assert.assertEquals(Long.valueOf(9007199254740993L), wireData.get("longValue")); + Assert.assertSame(Boolean.TRUE, wireData.get("active")); + Assert.assertSame(nested, wireData.get("address")); + Assert.assertSame(list, wireData.get("tags")); + Assert.assertEquals(Integer.valueOf(75001), ((Map) wireData.get("address")).get("zip")); + } + + @Test + public void testBulkInsert_recordOrderPreserved() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + List wireRecords = body.getRecords().get(); + Assert.assertEquals(7, wireRecords.size()); + for (int i = 0; i < 7; i++) { + Assert.assertEquals(Integer.valueOf(i), wireRecords.get(i).getData().get().get("pos")); + } + } + + @Test + public void testBulkInsert_recordOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + ArrayList records = new ArrayList<>(); + for (int i = 0; i < total; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + List> batches = Utils.createBulkInsertBatches(body.getRecords().get(), batchSize); + + Assert.assertEquals(3, batches.size()); + int expected = 0; + for (List batch : batches) { + for (V1InsertRecordData wire : batch) { + Assert.assertEquals(Integer.valueOf(expected), wire.getData().get().get("pos")); + // Non-batched per-record fields survive batching on every batch. + Assert.assertEquals("cards", wire.getTableName().get()); + expected++; + } + } + Assert.assertEquals(total, expected); + } + + @Test + public void testBulkInsert_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + ArrayList records = new ArrayList<>(); + for (int i = 0; i < total; i++) { + Map data = new HashMap<>(); + data.put("pos", i); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + List> batches = Utils.createBulkInsertBatches(body.getRecords().get(), batchSize); + + // Simulate the server echoing one response record per request record in each batch, then + // assert the SDK-assigned index equals the record's position in the ORIGINAL user list. + List indices = new ArrayList<>(); + List skyflowIds = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (V1InsertRecordData wire : batches.get(batchNumber)) { + responseRecords.add(V1RecordResponseObject.builder() + .skyflowId("sky-" + wire.getData().get().get("pos")) + .build()); + } + V1InsertResponse response = V1InsertResponse.builder().records(responseRecords).build(); + BulkInsertResponse formatted = Utils.formatBulkInsertResponse(response, batchNumber, batchSize, new HashMap<>()); + formatted.getRecords().forEach(r -> { + indices.add(r.getIndex()); + skyflowIds.add(r.getSkyflowId()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals("sky-" + i, skyflowIds.get(i)); + } + } + + // ── insert: table-name precedence / fallback regressions ───────────────── + + @Test + 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() + .tableName(" ") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals("cards", body.getTableName().get()); + } + + @Test + public void testBulkInsert_nullRecordTableNameStaysAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals("cards", body.getTableName().get()); + } + + @Test + public void testBulkInsert_emptyStringRecordTableNameStaysAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("").data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals("cards", body.getTableName().get()); + } + + @Test + public void testBulkInsert_recordTableNameOverridesRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("record_table") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("request_table") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertEquals("record_table", body.getRecords().get().get(0).getTableName().get()); + // The request-level name still goes out on the envelope, untouched. + Assert.assertEquals("request_table", body.getTableName().get()); + } + + @Test + public void testBulkInsert_perRecordTableNamesResolveIndependently() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord withOwn = BulkInsertRequestRecord.builder().tableName("own").data(data).build(); + BulkInsertRequestRecord blank = BulkInsertRequestRecord.builder().tableName(" ").data(data).build(); + BulkInsertRequestRecord missing = BulkInsertRequestRecord.builder().data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("fallback") + .records(recordList(withOwn, blank, missing)) + .build(); + + 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.assertFalse(body.getRecords().get().get(1).getTableName().isPresent()); + Assert.assertFalse(body.getRecords().get().get(2).getTableName().isPresent()); + Assert.assertEquals("fallback", body.getTableName().get()); + } + + @Test + public void testBulkInsert_blankRequestLevelTableNameIsOmittedFromEnvelope() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().tableName("cards").data(data).build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName(" ") + .records(recordList(record)) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getTableName().isPresent()); + Assert.assertEquals("cards", body.getRecords().get().get(0).getTableName().get()); + } + + // ── insert: upsert mapping ─────────────────────────────────────────────── + + @Test + public void testUpsert_updateTypeUpdateAndReplace_userValueReachesWire() { + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("UPDATE").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("REPLACE").getUpdateType().get()); + } + + @Test + public void testUpsert_updateTypeIsMatchedCaseInsensitively() { + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("update").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.UPDATE, upsertWire("UpDaTe").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("replace").getUpdateType().get()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, upsertWire("Replace").getUpdateType().get()); + } + + @Test + public void testUpsert_unrecognizedUpdateTypeIsRejectedBeforeMapping() { + // Validations.validateUpsertOptions now rejects anything that is not UPDATE/REPLACE, so + // the mapper can no longer be reached with a value it would silently drop. A null + // updateType stays legal and simply omits the field. + com.skyflow.generated.rest.types.V1Upsert nullType = upsertWire(null); + Assert.assertFalse(nullType.getUpdateType().isPresent()); + Assert.assertEquals(Collections.singletonList("email"), nullType.getUniqueColumns().get()); + } + + @Test + public void testUpsert_uniqueColumnsValuesAndOrderReachWire() { + List uniqueColumns = Arrays.asList("email", "phone number", NON_ASCII_NAME); + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder().updateType("UPDATE").uniqueColumns(uniqueColumns).build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + List wireColumns = body.getRecords().get().get(0).getUpsert().get().getUniqueColumns().get(); + Assert.assertSame(uniqueColumns, wireColumns); + Assert.assertEquals(Arrays.asList("email", "phone number", NON_ASCII_NAME), wireColumns); + } + + @Test + public void testUpsert_recordLevelOverridesRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Collections.singletonList("record_col")) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder() + .records(recordList(record)) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("request_col")) + .build()) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + com.skyflow.generated.rest.types.V1Upsert wire = body.getRecords().get().get(0).getUpsert().get(); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, wire.getUpdateType().get()); + Assert.assertEquals(Collections.singletonList("record_col"), wire.getUniqueColumns().get()); + } + + @Test + public void testUpsert_requestLevelStaysOnEnvelopeAndIsNotCopiedOntoRecords() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(recordList( + BulkInsertRequestRecord.builder().data(data).build(), + BulkInsertRequestRecord.builder().data(data).build(), + BulkInsertRequestRecord.builder().data(data).build())) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + + 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.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_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() + .tableName("cards") + .records(recordList(BulkInsertRequestRecord.builder().data(data).build())) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertTrue(body.getUpsert().isPresent()); + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); + } + + @Test + public void testUpsert_emptyUniqueColumnsMeansNoUpsertOnWire() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType("UPDATE") + .uniqueColumns(new ArrayList<>()) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getUpsert().isPresent()); + } + + private static com.skyflow.generated.rest.types.V1Upsert upsertWire(String updateType) { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .upsert(UpsertOptions.builder() + .updateType(updateType) + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + return Utils.getBulkInsertRequestBody(request, vaultConfig()) + .getRecords().get().get(0).getUpsert().get(); + } + + // ── insert: tokens map ─────────────────────────────────────────────────── + + @Test + public void testBulkInsert_emptyTokensMapIsOmittedFromWire() { + // Validations.validateInsertRequest rejects an explicitly-set-but-empty tokens map before + // the body builder runs; this pins the builder's own behavior when called directly. + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(new HashMap<>()) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTokens().isPresent()); + } + + @Test + public void testBulkInsert_nullTokensMapIsOmittedFromWire() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Assert.assertFalse(body.getRecords().get().get(0).getTokens().isPresent()); + } + + @Test + public void testBulkInsert_multiValueTokensMapReachesWireVerbatim() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new LinkedHashMap<>(); + tokens.put("name", "tok-1"); + tokens.put("ssn", "tok-2"); + tokens.put("nested", Collections.singletonMap("group", "tok-3")); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("cards") + .data(data) + .tokens(tokens) + .build(); + BulkInsertRequest request = BulkInsertRequest.builder().records(recordList(record)).build(); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, vaultConfig()); + + Map wireTokens = body.getRecords().get().get(0).getTokens().get(); + Assert.assertSame(tokens, wireTokens); + Assert.assertEquals("tok-1", wireTokens.get("name")); + Assert.assertEquals("tok-2", wireTokens.get("ssn")); + Assert.assertEquals(Collections.singletonMap("group", "tok-3"), wireTokens.get("nested")); + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk detokenize + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkDetokenize_everyFieldReachesWire() { + List tokens = Arrays.asList("token-1", "token 2", "トークン-3"); + TokenGroupRedactions groupA = TokenGroupRedactions.builder() + .tokenGroupName("group one") + .redaction("MASKED") + .build(); + TokenGroupRedactions groupB = TokenGroupRedactions.builder() + .tokenGroupName(NON_ASCII_NAME) + .redaction("PLAIN_TEXT") + .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Arrays.asList(groupA, groupB)) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertSame(tokens, body.getTokens().get()); + Assert.assertEquals(Arrays.asList("token-1", "token 2", "トークン-3"), body.getTokens().get()); + + List wireGroups = body.getTokenGroupRedactions().get(); + Assert.assertEquals(2, wireGroups.size()); + Assert.assertEquals("group one", wireGroups.get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", wireGroups.get(0).getRedaction().get()); + Assert.assertEquals(NON_ASCII_NAME, wireGroups.get(1).getTokenGroupName().get()); + Assert.assertEquals("PLAIN_TEXT", wireGroups.get(1).getRedaction().get()); + } + + @Test + public void testBulkDetokenize_emptyTokenGroupRedactionsListIsOmitted() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token-1")) + .tokenGroupRedactions(new ArrayList<>()) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + + Assert.assertFalse(body.getTokenGroupRedactions().isPresent()); + } + + @Test + public void testBulkDetokenize_tokenOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + List flattened = new ArrayList<>(); + for (V1FlowDetokenizeRequest batch : batches) { + flattened.addAll(batch.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + } + + @Test + public void testBulkDetokenize_vaultIdAndRedactionsPresentOnEveryBatch() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + TokenGroupRedactions group = TokenGroupRedactions.builder() + .tokenGroupName("group one") + .redaction("MASKED") + .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Collections.singletonList(group)) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + for (V1FlowDetokenizeRequest batch : batches) { + Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); + Assert.assertTrue(batch.getTokenGroupRedactions().isPresent()); + Assert.assertEquals(1, batch.getTokenGroupRedactions().get().size()); + Assert.assertEquals("group one", batch.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", batch.getTokenGroupRedactions().get().get(0).getRedaction().get()); + } + } + + @Test + public void testBulkDetokenize_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDetokenizeBatches(body, batchSize); + + List indices = new ArrayList<>(); + List echoedTokens = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (String token : batches.get(batchNumber).getTokens().get()) { + responseRecords.add(V1FlowDetokenizeResponseObject.builder().token(token).build()); + } + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder().response(responseRecords).build(); + BulkDetokenizeResponse formatted = + Utils.formatBulkDetokenizeResponse(response, batchNumber, batchSize, new HashMap<>()); + formatted.getRecords().forEach(r -> { + indices.add(r.getIndex()); + echoedTokens.add(r.getToken()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals(tokens.get(i), echoedTokens.get(i)); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk tokenize + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkTokenize_everyFieldReachesWire() { + List groupNames = Arrays.asList("group one", NON_ASCII_NAME); + BulkTokenizeRequestRecord record = BulkTokenizeRequestRecord.builder() + .value(NON_ASCII_NAME) + .tokenGroupNames(groupNames) + .build(); + List records = Collections.singletonList(record); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertEquals(1, body.getData().get().size()); + V1FlowTokenizeRequestObject wire = body.getData().get().get(0); + Assert.assertEquals(NON_ASCII_NAME, wire.getValue().get()); + Assert.assertSame(groupNames, wire.getTokenGroupNames().get()); + 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<>(); + nested.put("city", "Paris"); + nested.put("zip", 75001); + List listValue = Arrays.asList(1, 2, 3); + + 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(records, VAULT_ID); + List wire = body.getData().get(); + + Assert.assertEquals(Integer.valueOf(42), wire.get(0).getValue().get()); + Assert.assertEquals(Double.valueOf(3.14d), wire.get(1).getValue().get()); + Assert.assertSame(Boolean.FALSE, wire.get(2).getValue().get()); + Assert.assertSame(nested, wire.get(3).getValue().get()); + Assert.assertSame(listValue, wire.get(4).getValue().get()); + } + + @Test + public void testBulkTokenize_nullTokenGroupNamesIsOmitted() { + List records = + Collections.singletonList(BulkTokenizeRequestRecord.builder().value("v1").build()); + + V1FlowTokenizeRequest body = Utils.getBulkTokenizeRequestBody(records, VAULT_ID); + + Assert.assertFalse(body.getData().get().get(0).getTokenGroupNames().isPresent()); + } + + @Test + public void testBulkTokenize_recordOrderPreservedAcrossBatches() { + int total = 7; + int batchSize = 3; + List records = new ArrayList<>(); + for (int i = 0; i < total; i++) { + records.add(BulkTokenizeRequestRecord.builder().value("value-" + i).build()); + } + + // 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 (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, body.getVaultId().get()); + for (V1FlowTokenizeRequestObject obj : body.getData().get()) { + flattened.add(obj.getValue().get()); + } + } + Assert.assertEquals(total, flattened.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals("value-" + i, flattened.get(i)); + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Bulk delete tokens + // ───────────────────────────────────────────────────────────────────────── + + @Test + public void testBulkDeleteTokens_everyFieldReachesWire() { + List tokens = Arrays.asList("token-1", "token 2", "トークン-3"); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + + Assert.assertEquals(VAULT_ID, body.getVaultId().get()); + Assert.assertSame(tokens, body.getTokens().get()); + Assert.assertEquals(Arrays.asList("token-1", "token 2", "トークン-3"), body.getTokens().get()); + } + + @Test + public void testBulkDeleteTokens_tokenOrderPreservedAndVaultIdOnEveryBatch() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDeleteTokensBatches(body, batchSize); + + Assert.assertEquals(3, batches.size()); + List flattened = new ArrayList<>(); + for (V1FlowDeleteTokenRequest batch : batches) { + Assert.assertEquals(VAULT_ID, batch.getVaultId().get()); + flattened.addAll(batch.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + } + + @Test + public void testBulkDeleteTokens_responseIndexMapsToOriginalInputPosition_acrossBatches() { + int total = 7; + int batchSize = 3; + List tokens = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, VAULT_ID); + List batches = Utils.createBulkDeleteTokensBatches(body, batchSize); + + List indices = new ArrayList<>(); + List echoed = new ArrayList<>(); + for (int batchNumber = 0; batchNumber < batches.size(); batchNumber++) { + List responseRecords = new ArrayList<>(); + for (String token : batches.get(batchNumber).getTokens().get()) { + responseRecords.add(V1DeleteTokenResponseObject.builder().value(token).build()); + } + V1FlowDeleteTokenResponse response = + V1FlowDeleteTokenResponse.builder().tokens(responseRecords).build(); + // 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()); + }); + } + + Assert.assertEquals(total, indices.size()); + for (int i = 0; i < total; i++) { + Assert.assertEquals(Integer.valueOf(i), indices.get(i)); + Assert.assertEquals(tokens.get(i), echoed.get(i)); + } + } +} 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..753487d8 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/RequestIdTests.java @@ -0,0 +1,454 @@ +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_transportFailureReportsTheInnermostCause() { + // a mistyped cluster id surfaces as UnknownHostException three levels down: the future + // wraps ApiClientException("Network error..."), which wraps the real cause. Reporting the + // wrapper tells the caller nothing, so the innermost cause must win. + java.net.UnknownHostException dns = new java.net.UnknownHostException( + "badcluster.skyvault.skyflowapis.dev: nodename nor servname provided, or not known"); + Throwable ex = new RuntimeException( + new com.skyflow.generated.rest.core.ApiClientException( + "Network error executing HTTP request", dns)); + + List records = Utils.handleBulkTokenizeBatchException( + ex, Collections.singletonList(tokenizeRecord("v0", "g1")), 0); + + String error = records.get(0).getTokens().get(0).getError(); + Assert.assertTrue("expected the DNS failure, got: " + error, + error.contains("UnknownHostException")); + Assert.assertTrue(error.contains("badcluster.skyvault.skyflowapis.dev")); + } + + @Test + public void testDelete_transportFailureReportsTheInnermostCause() { + java.net.UnknownHostException dns = new java.net.UnknownHostException( + "badcluster.skyvault.skyflowapis.dev: nodename nor servname provided, or not known"); + Throwable ex = new RuntimeException( + new com.skyflow.generated.rest.core.ApiClientException( + "Network error executing HTTP request", dns)); + + List records = + Utils.handleBulkDeleteTokensBatchException(ex, deleteBatch("t0", "t1"), 0, 50); + + Assert.assertEquals(2, records.size()); + for (BulkDeleteTokensResponseRecord record : records) { + Assert.assertTrue("expected the DNS failure, got: " + record.getError(), + record.getError().contains("UnknownHostException")); + Assert.assertEquals(Integer.valueOf(500), record.getHttpCode()); + } + } + + @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 new file mode 100644 index 00000000..4e034bfd --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -0,0 +1,2045 @@ +package com.skyflow.utils; + +import com.google.gson.JsonObject; +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.ApiClientApiException; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest; +import com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest; +import com.skyflow.generated.rest.types.FlowEnumUpdateType; +import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.V1InsertRecordData; +import com.skyflow.generated.rest.types.V1InsertResponse; +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.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; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UtilsTests { + + private static final String ENV_FILE = ".env"; + private byte[] originalEnvContent; + + @Before + public void saveEnvFileState() throws IOException { + File f = new File(ENV_FILE); + originalEnvContent = f.exists() ? Files.readAllBytes(Paths.get(ENV_FILE)) : null; + } + + @After + public void restoreEnvFile() throws IOException { + if (originalEnvContent != null) { + Files.write(Paths.get(ENV_FILE), originalEnvContent); + } else { + Files.deleteIfExists(Paths.get(ENV_FILE)); + } + } + + // ── getVaultUrl ─────────────────────────────────────────────────────────── + + @Test + public void testGetVaultURL_prodEnv() { + String url = Utils.getVaultUrl("cluster1", Env.PROD); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.com", url); + } + + @Test + public void testGetVaultURL_devEnv() { + String url = Utils.getVaultUrl("cluster1", Env.DEV); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.dev", url); + } + + @Test + public void testGetVaultURL_stageEnv() { + String url = Utils.getVaultUrl("cluster1", Env.STAGE); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis.tech", url); + } + + @Test + public void testGetVaultURL_sandboxEnv() { + String url = Utils.getVaultUrl("cluster1", Env.SANDBOX); + Assert.assertEquals("https://cluster1.skyvault.skyflowapis-preview.com", url); + } + + // ── getMetrics ──────────────────────────────────────────────────────────── + + @Test + public void testGetMetrics_containsSdkVersion() { + JsonObject metrics = Utils.getMetrics(); + Assert.assertTrue(metrics.has(BaseConstants.SDK_METRIC_NAME_VERSION)); + String sdkVersionMetric = metrics.get(BaseConstants.SDK_METRIC_NAME_VERSION).getAsString(); + Assert.assertTrue(sdkVersionMetric.startsWith(Constants.SDK_METRIC_NAME_VERSION_PREFIX)); + } + + // ── getEnvVaultUrl ──────────────────────────────────────────────────────── + + @Test + public void testGetEnvVaultURL_doesNotThrowUnexpectedException() { + try { + Utils.getEnvVaultUrl(); + } catch (SkyflowException e) { + // acceptable if this environment happens to have an invalid VAULT_URL set + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testGetEnvVaultURL_emptyValueThrowsEmptyVaultUrl() throws Exception { + // No VAULT_URL system/real env var is set in the test environment, so this + // exercises the Dotenv.load() fallback path with an empty value. + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write("VAULT_URL=\n"); + } + + try { + Utils.getEnvVaultUrl(); + Assert.fail("Should have thrown SkyflowException for empty VAULT_URL"); + } catch (SkyflowException e) { + Assert.assertEquals(com.skyflow.errors.ErrorMessage.EmptyVaultUrl.getMessage(), e.getMessage()); + } + } + + @Test + public void testGetEnvVaultURL_invalidFormatThrowsInvalidVaultUrlFormat() throws Exception { + try (FileWriter fw = new FileWriter(ENV_FILE)) { + fw.write("VAULT_URL=http://example.com\n"); + } + + try { + Utils.getEnvVaultUrl(); + Assert.fail("Should have thrown SkyflowException for invalid VAULT_URL format"); + } catch (SkyflowException e) { + Assert.assertEquals(com.skyflow.errors.ErrorMessage.InvalidVaultUrlFormat.getMessage(), e.getMessage()); + } + } + + // ── isValidUrl ──────────────────────────────────────────────────────────── + + @Test + public void testIsValidURL_validHttpsUrl() { + Assert.assertTrue(Utils.isValidUrl("https://example.com")); + } + + @Test + public void testIsValidURL_httpUrlIsInvalid() { + Assert.assertFalse(Utils.isValidUrl("http://example.com")); + } + + @Test + public void testIsValidURL_malformedUrl() { + Assert.assertFalse(Utils.isValidUrl("not a url")); + } + + @Test + public void testIsValidURL_httpsUrlWithEmptyHostIsInvalid() { + Assert.assertFalse(Utils.isValidUrl("https:///path")); + } + + // ── generateBearerToken ─────────────────────────────────────────────────── + + @Test + public void testGenerateBearerToken_withDirectToken() throws SkyflowException { + Credentials credentials = new Credentials(); + credentials.setToken("direct-token-value"); + + String token = Utils.generateBearerToken(credentials); + + Assert.assertEquals("direct-token-value", token); + } + + @Test + public void testGenerateBearerToken_withInvalidCredentialsStringThrows() { + Credentials credentials = new Credentials(); + // A string that fails JSON syntax parsing (as opposed to e.g. a bare word, + // which Gson's lenient parser accepts as a JSON primitive rather than rejecting outright). + credentials.setCredentialsString("./src/test/credentials.json"); + try { + Utils.generateBearerToken(credentials); + Assert.fail("Should have thrown an exception"); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testGenerateBearerToken_withNonExistentPathThrows() { + Credentials credentials = new Credentials(); + credentials.setPath("/nonexistent/path/credentials.json"); + try { + Utils.generateBearerToken(credentials); + Assert.fail("Should have thrown an exception"); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── getInsertRequestBody (unary) ────────────────────────────────────────── + + @Test + public void testGetInsertRequestBody_buildsCorrectRequest() { + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + InsertRequest request = InsertRequest.builder().records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getInsertRequestBody(request, config); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals(1, body.getRecords().get().size()); + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); + } + + @Test + public void testGetInsertRequestBody_keepsRequestLevelTableNameOnEnvelopeOnly() { + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord record = InsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getInsertRequestBody(request, config); + + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + } + + @Test + public void testGetInsertRequestBody_withTokens() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).tokens(tokens).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + InsertRequest request = InsertRequest.builder().records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getInsertRequestBody(request, config); + + Assert.assertEquals(tokens, body.getRecords().get().get(0).getTokens().get()); + } + + @Test + public void testGetInsertRequestBody_withUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + InsertRequestRecord record = InsertRequestRecord.builder().tableName("table1").data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") + .build(); + InsertRequest request = InsertRequest.builder() + .records(records) + .upsert(upsert) + .build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getInsertRequestBody(request, config); + + // 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 + public void testGetInsertRequestBody_withUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") + .build(); + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(upsert) + .build(); + ArrayList records = new ArrayList<>(); + records.add(record); + InsertRequest request = InsertRequest.builder().records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getInsertRequestBody(request, config); + + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); + } + + // Tests for buildInsertResponse / getDetokenizeRequestBody / buildDetokenizeResponse / + // getTokenizeRequestBody / buildTokenizeResponse / getDeleteTokensRequestBody / + // buildDeleteTokensResponse were removed: those unary Utils helpers no longer exist (bulk-only module). + + // ── getBulkInsertRequestBody (bulk overload) ────────────────────────────── + + @Test + public void testGetBulkInsertRequestBody_bulk_buildsCorrectRequest() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals("table1", body.getTableName().get()); + Assert.assertEquals(1, body.getRecords().get().size()); + Assert.assertFalse(body.getRecords().get().get(0).getTableName().isPresent()); + Assert.assertEquals(data, body.getRecords().get().get(0).getData().get()); + } + + @Test + public void testGetBulkInsertRequestBody_bulk_withUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().data(data).build(); + ArrayList records = new ArrayList<>(); + records.add(record); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") + .build()) + .build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); + + // 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 + public void testGetBulkInsertRequestBody_bulk_withUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") + .build()) + .build(); + ArrayList records = new ArrayList<>(); + records.add(record); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + + V1InsertRequest body = Utils.getBulkInsertRequestBody(request, config); + + Assert.assertEquals("table1", body.getRecords().get().get(0).getTableName().get()); + Assert.assertTrue(body.getRecords().get().get(0).getUpsert().isPresent()); + Assert.assertEquals(FlowEnumUpdateType.REPLACE, body.getRecords().get().get(0).getUpsert().get().getUpdateType().get()); + } + + // ── getBulkDetokenizeRequestBody ────────────────────────────────────────── + + @Test + public void testGetBulkDetokenizeRequestBody_buildsCorrectRequest() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Arrays.asList("token1", "token2")) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, "vault123"); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals(Arrays.asList("token1", "token2"), body.getTokens().get()); + Assert.assertFalse(body.getTokenGroupRedactions().isPresent()); + } + + @Test + public void testGetBulkDetokenizeRequestBody_withTokenGroupRedactions() { + TokenGroupRedactions redaction = TokenGroupRedactions.builder() + .tokenGroupName("group1") + .redaction("MASKED") + .build(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(Collections.singletonList(redaction)) + .build(); + + V1FlowDetokenizeRequest body = Utils.getBulkDetokenizeRequestBody(request, "vault123"); + + Assert.assertTrue(body.getTokenGroupRedactions().isPresent()); + Assert.assertEquals(1, body.getTokenGroupRedactions().get().size()); + Assert.assertEquals("group1", body.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", body.getTokenGroupRedactions().get().get(0).getRedaction().get()); + } + + // ── getBulkDeleteTokensRequestBody ───────────────────────────────────────── + + @Test + public void testGetBulkDeleteTokensRequestBody_buildsCorrectRequest() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Arrays.asList("token1", "token2")) + .build(); + + V1FlowDeleteTokenRequest body = Utils.getBulkDeleteTokensRequestBody(request, "vault123"); + + Assert.assertEquals("vault123", body.getVaultId().get()); + Assert.assertEquals(Arrays.asList("token1", "token2"), body.getTokens().get()); + } + + // ── getBulkTokenizeRequestBody ───────────────────────────────────────────── + + @Test + public void testGetBulkTokenizeRequestBody_buildsCorrectRequest() { + List records = Collections.singletonList( + BulkTokenizeRequestRecord.builder() + .value("value1") + .tokenGroupNames(Collections.singletonList("group1")) + .build()); + + 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 ──────────────────────────────────────────────── + + @Test + public void testCreateBulkInsertBatches_splitsEvenly() { + List records = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + records.add(V1InsertRecordData.builder().data(new HashMap<>()).build()); + } + + List> batches = Utils.createBulkInsertBatches(records, 2); + + Assert.assertEquals(2, batches.size()); + Assert.assertEquals(2, batches.get(0).size()); + Assert.assertEquals(2, batches.get(1).size()); + } + + @Test + public void testCreateBulkInsertBatches_splitsWithRemainder() { + List records = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + records.add(V1InsertRecordData.builder().data(new HashMap<>()).build()); + } + + List> batches = Utils.createBulkInsertBatches(records, 2); + + Assert.assertEquals(3, batches.size()); + Assert.assertEquals(1, batches.get(2).size()); + } + + // ── createBulkDetokenizeBatches ──────────────────────────────────────────── + + @Test + public void testCreateBulkDetokenizeBatches_splitsTokens() { + V1FlowDetokenizeRequest request = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2", "t3")) + .build(); + + List batches = Utils.createBulkDetokenizeBatches(request, 2); + + Assert.assertEquals(2, batches.size()); + Assert.assertEquals(Arrays.asList("t1", "t2"), batches.get(0).getTokens().get()); + Assert.assertEquals(Collections.singletonList("t3"), batches.get(1).getTokens().get()); + Assert.assertEquals("vault123", batches.get(0).getVaultId().get()); + } + + @Test + public void testCreateBulkDetokenizeBatches_carriesTokenGroupRedactions() { + com.skyflow.generated.rest.types.V1TokenGroupRedactions redaction = + com.skyflow.generated.rest.types.V1TokenGroupRedactions.builder() + .tokenGroupName("group1") + .redaction("MASKED") + .build(); + V1FlowDetokenizeRequest request = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .tokenGroupRedactions(Collections.singletonList(redaction)) + .build(); + + List batches = Utils.createBulkDetokenizeBatches(request, 5); + + Assert.assertEquals(1, batches.size()); + Assert.assertTrue(batches.get(0).getTokenGroupRedactions().isPresent()); + Assert.assertEquals("group1", batches.get(0).getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + } + + // ── createBulkDeleteTokensBatches ────────────────────────────────────────── + + @Test + public void testCreateBulkDeleteTokensBatches_splitsTokens() { + V1FlowDeleteTokenRequest request = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2", "t3")) + .build(); + + List batches = Utils.createBulkDeleteTokensBatches(request, 2); + + Assert.assertEquals(2, batches.size()); + Assert.assertEquals(Arrays.asList("t1", "t2"), batches.get(0).getTokens().get()); + Assert.assertEquals(Collections.singletonList("t3"), batches.get(1).getTokens().get()); + } + + // ── createBulkTokenizeBatches ────────────────────────────────────────────── + + @Test + public void testCreateBulkTokenizeBatches_splitsData() { + List records = Arrays.asList( + BulkTokenizeRequestRecord.builder().value("v1").build(), + BulkTokenizeRequestRecord.builder().value("v2").build()); + + List> batches = Utils.createBulkTokenizeBatches(records, 1); + + Assert.assertEquals(2, batches.size()); + 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 ────────────────────────────────────────────────────── + + @Test + public void testCreateErrorRecord_withHttpCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("http_code", 400); + recordMap.put("error", "bad request"); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 0, "req-1"); + + Assert.assertEquals(400, err.getCode()); + Assert.assertEquals("bad request", err.getError()); + Assert.assertEquals("req-1", err.getRequestId()); + } + + @Test + public void testCreateErrorRecord_withStatusCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("statusCode", 500); + recordMap.put("message", "server error"); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 2, null); + + Assert.assertEquals(500, err.getCode()); + Assert.assertEquals("server error", err.getError()); + Assert.assertEquals(2, err.getIndex()); + } + + @Test + public void testCreateErrorRecord_nullMapReturnsNull() { + Assert.assertNull(Utils.createErrorRecord(null, 0, null)); + } + + @Test + public void testCreateErrorRecord_noCodeKeyDefaultsTo500() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "something went wrong"); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 1, "req-2"); + + Assert.assertEquals(500, err.getCode()); + Assert.assertEquals("something went wrong", err.getError()); + } + + @Test + public void testCreateErrorRecord_noErrorOrMessageKeyDefaultsToUnknownError() { + Map recordMap = new HashMap<>(); + recordMap.put("http_code", 403); + + ErrorRecord err = Utils.createErrorRecord(recordMap, 3, null); + + Assert.assertEquals(403, err.getCode()); + Assert.assertEquals("Unknown error", err.getError()); + } + + // ── handleBulkInsertBatchException ──────────────────────────────────────── + + @Test + public void testHandleBulkInsertBatchException_apiExceptionWithRecordsBody() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("error", "duplicate"); + errorRecordMap.put("httpCode", 409); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 409, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(409, errors.get(0).getHttpCode()); + Assert.assertEquals("duplicate", errors.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_apiExceptionWithNoParsableBody() { + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 401, "unauthorized"); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Arrays.asList( + V1InsertRecordData.builder().data(new HashMap<>()).build(), + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals(401, errors.get(0).getHttpCode()); + Assert.assertEquals("insert failed", errors.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_apiExceptionWithErrorKeyBody() { + Map body = new HashMap<>(); + body.put("error", "top level auth error"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); + List errors = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(401, errors.get(0).getHttpCode()); + Assert.assertEquals("top level auth error", errors.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_genericException() { + RuntimeException ex = new RuntimeException("boom"); + List batch = Collections.singletonList(V1InsertRecordData.builder().data(new HashMap<>()).build()); + + List errors = Utils.handleBulkInsertBatchException(ex, batch, 1, 2); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(500, errors.get(0).getHttpCode()); + Assert.assertEquals("boom", errors.get(0).getError()); + Assert.assertEquals(2, errors.get(0).getIndex()); + // Projected error records carry no table/id/field data. + Assert.assertNull(errors.get(0).getTableName()); + Assert.assertNull(errors.get(0).getSkyflowId()); + Assert.assertNull(errors.get(0).getFields()); + Assert.assertNull(errors.get(0).getHashedData()); + } + + @Test + public void testHandleBulkInsertBatchException_recordsBodyCarriesTableAndSkyflowId() { + // createInsertErrorRecord now builds a BulkInsertResponseRecord directly, so per-record + // table/id data from the error body survives instead of being nulled out. + Map recordMap = new HashMap<>(); + recordMap.put("tableName", "cards"); + recordMap.put("error", "duplicate"); + recordMap.put("httpCode", 409); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(recordMap)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 409, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("cards", records.get(0).getTableName()); + Assert.assertEquals("duplicate", records.get(0).getError()); + Assert.assertEquals(409, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkInsertBatchException_recordsBodyFallsBackToUnknownError() { + // An entry with no error/message key still produces a record rather than being skipped. + Map recordMap = new HashMap<>(); + recordMap.put("httpCode", 500); + Map body = new HashMap<>(); + body.put("records", Collections.singletonList(recordMap)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("Unknown error", records.get(0).getError()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkInsertBatchException_indexOffsetAcrossBatches() { + Map body = new HashMap<>(); + body.put("error", "auth error"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Arrays.asList( + V1InsertRecordData.builder().data(new HashMap<>()).build(), + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 2, 50); + + Assert.assertEquals(100, records.get(0).getIndex()); + Assert.assertEquals(101, records.get(1).getIndex()); + } + + @Test + public void testHandleBulkInsertBatchException_genericExceptionHasNoRequestId() { + // A non-API failure has no response headers to read a request id from. + RuntimeException ex = new RuntimeException("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.assertNull(records.get(0).getRequestId()); + 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 + public void testCreateInsertErrorRecord_nullRecordMapReturnsNull() { + Assert.assertNull(Utils.createInsertErrorRecord(null, 0, "req-1")); + Assert.assertNull(Utils.createDetokenizeErrorRecord(null, 0, "req-1")); + } + + @Test + public void testCreateErrorRecords_snakeCaseHttpCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("http_code", 409); + recordMap.put("error", "duplicate"); + + Assert.assertEquals(409, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(409, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_statusCodeKey() { + Map recordMap = new HashMap<>(); + recordMap.put("statusCode", 422); + recordMap.put("error", "unprocessable"); + + Assert.assertEquals(422, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + Assert.assertEquals(422, Utils.createDetokenizeErrorRecord(recordMap, 0, null).getHttpCode()); + } + + @Test + public void testCreateErrorRecords_noHttpCodeKeyDefaultsTo500() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "no code supplied"); + + Assert.assertEquals(500, Utils.createInsertErrorRecord(recordMap, 0, null).getHttpCode()); + 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<>(); + recordMap.put("message", "vault not found"); + recordMap.put("httpCode", 404); + + Assert.assertEquals("vault not found", Utils.createInsertErrorRecord(recordMap, 0, null).getError()); + Assert.assertEquals("vault not found", Utils.createDetokenizeErrorRecord(recordMap, 0, null).getError()); + } + + @Test + public void testCreateInsertErrorRecord_readsSkyflowIdUsingWireCasing() { + // The API returns the id as "skyflowID" — matching @JsonProperty("skyflowID") on the + // generated V1RecordResponseObject — even though the SDK exposes it as getSkyflowId(). + Map recordMap = new HashMap<>(); + recordMap.put("skyflowID", "id-1"); + recordMap.put("tableName", "cards"); + recordMap.put("error", "duplicate"); + + BulkInsertResponseRecord record = Utils.createInsertErrorRecord(recordMap, 0, null); + + Assert.assertEquals("id-1", record.getSkyflowId()); + Assert.assertEquals("cards", record.getTableName()); + } + + @Test + public void testCreateErrorRecords_requestIdIsCarried() { + Map recordMap = new HashMap<>(); + recordMap.put("error", "boom"); + + Assert.assertEquals("req-7", Utils.createInsertErrorRecord(recordMap, 3, "req-7").getRequestId()); + Assert.assertEquals("req-7", Utils.createDetokenizeErrorRecord(recordMap, 3, "req-7").getRequestId()); + Assert.assertEquals(3, Utils.createInsertErrorRecord(recordMap, 3, "req-7").getIndex()); + } + + // ── handleBulkInsertBatchException, remaining branches ──────────────────── + + @Test + public void testHandleBulkInsertBatchException_errorFieldAsObjectUsesHelper() { + // The API's structured error envelope: {"error": {message, httpCode, ...}} + 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("insert failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Arrays.asList( + V1InsertRecordData.builder().data(new HashMap<>()).build(), + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(2, records.size()); + for (BulkInsertResponseRecord record : records) { + Assert.assertEquals("vault not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + } + } + + @Test + public void testHandleBulkInsertBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_recordsNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("records", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + Assert.assertEquals(400, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkInsertBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("records", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + // No entry parsed, so the batch-wide fallback fires instead. + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkInsertBatchException_bodyWithNeitherRecordsNorErrorKey() { + // A map body that matches neither branch falls through to the batch-wide fallback. + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("insert failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + List batch = Collections.singletonList( + V1InsertRecordData.builder().data(new HashMap<>()).build()); + List records = Utils.handleBulkInsertBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("insert failed", records.get(0).getError()); + Assert.assertEquals(503, records.get(0).getHttpCode()); + } + + // ── handleBulkDetokenizeBatchException ──────────────────────────────────── + + @Test + public void testHandleBulkDetokenizeBatchException_apiExceptionWithResponseBody() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("error", "token not found"); + errorRecordMap.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List errors = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(404, errors.get(0).getHttpCode()); + Assert.assertEquals("token not found", errors.get(0).getError()); + Assert.assertEquals(0, errors.get(0).getIndex()); + // This entry carried no token/group of its own, so those stay null. + Assert.assertNull(errors.get(0).getToken()); + Assert.assertNull(errors.get(0).getTokenGroupName()); + Assert.assertNull(errors.get(0).getMetadata()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseBodyCarriesTokenAndGroup() { + // createDetokenizeErrorRecord builds a BulkDetokenizeResponseRecord directly, so the + // failing token echoed back by the API survives instead of being nulled out. + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("token", "tok-bad"); + errorRecordMap.put("tokenGroupName", "email_group"); + errorRecordMap.put("error", "token not found"); + errorRecordMap.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("tok-bad")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("tok-bad", records.get(0).getToken()); + Assert.assertEquals("email_group", records.get(0).getTokenGroupName()); + Assert.assertEquals("token not found", records.get(0).getError()); + Assert.assertEquals(404, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseBodyFallsBackToUnknownError() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("httpCode", 500); + Map body = new HashMap<>(); + body.put("response", Collections.singletonList(errorRecordMap)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("Unknown error", records.get(0).getError()); + Assert.assertEquals(500, records.get(0).getHttpCode()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_topLevelErrorAppliesToEveryToken() { + Map body = new HashMap<>(); + body.put("error", "top level auth error"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 2, 50); + + Assert.assertEquals(2, records.size()); + Assert.assertEquals("top level auth error", records.get(0).getError()); + Assert.assertEquals(401, records.get(0).getHttpCode()); + // index continues from the batch offset + Assert.assertEquals(100, records.get(0).getIndex()); + Assert.assertEquals(101, records.get(1).getIndex()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_errorFieldAsObjectUsesHelper() { + 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("detokenize failed", 404, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Arrays.asList("t1", "t2")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(2, records.size()); + for (BulkDetokenizeResponseRecord record : records) { + Assert.assertEquals("vault not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + } + } + + @Test + public void testHandleBulkDetokenizeBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 500, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_responseNotAListFallsBackToBatchWideError() { + Map body = new HashMap<>(); + body.put("response", "not-a-list"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_nonMapEntriesAreSkipped() { + Map body = new HashMap<>(); + body.put("response", Arrays.asList("not-a-map", null)); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 400, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_batchWithNoTokensProducesNoRecords() { + Map body = new HashMap<>(); + body.put("error", "auth error"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 401, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder().vaultId("vault123").build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertTrue(records.isEmpty()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_bodyWithNeitherResponseNorErrorKey() { + Map body = new HashMap<>(); + body.put("unexpected", "shape"); + ApiClientApiException apiEx = new ApiClientApiException("detokenize failed", 503, body); + RuntimeException wrapper = new RuntimeException(apiEx); + + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + List records = Utils.handleBulkDetokenizeBatchException(wrapper, batch, 0, 50); + + Assert.assertEquals(1, records.size()); + Assert.assertEquals("detokenize failed", records.get(0).getError()); + Assert.assertEquals(503, records.get(0).getHttpCode()); + } + + // ── formatBulk*Response, empty/absent bodies ────────────────────────────── + + @Test + public void testFormatBulkResponses_nullResponseReturnsNull() { + Assert.assertNull(Utils.formatBulkInsertResponse(null, 0, 50, null)); + Assert.assertNull(Utils.formatBulkDetokenizeResponse(null, 0, 50, null)); + } + + @Test + public void testFormatBulkResponses_absentRecordsReturnsNull() { + Assert.assertNull(Utils.formatBulkInsertResponse( + V1InsertResponse.builder().build(), 0, 50, null)); + Assert.assertNull(Utils.formatBulkDetokenizeResponse( + V1FlowDetokenizeResponse.builder().build(), 0, 50, null)); + } + + @Test + public void testHandleBulkDetokenizeBatchException_genericExceptionHasNoRequestId() { + RuntimeException ex = new RuntimeException("boom"); + 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.assertNull(records.get(0).getRequestId()); + Assert.assertEquals("boom", records.get(0).getError()); + } + + @Test + public void testHandleBulkDetokenizeBatchException_genericException() { + RuntimeException ex = new RuntimeException("boom"); + V1FlowDetokenizeRequest batch = V1FlowDetokenizeRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + + List errors = Utils.handleBulkDetokenizeBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(500, errors.get(0).getHttpCode()); + 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 + public void testHandleBulkDeleteTokensBatchException_apiExceptionWithTokensBody() { + Map errorRecordMap = new HashMap<>(); + errorRecordMap.put("error", "not found"); + errorRecordMap.put("httpCode", 404); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(errorRecordMap)); + 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("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 + public void testHandleBulkDeleteTokensBatchException_apiExceptionWithErrorKeyBody() { + Map body = new HashMap<>(); + body.put("error", "top level delete error"); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 403, 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(403), errors.get(0).getHttpCode()); + Assert.assertEquals("top level delete error", errors.get(0).getError()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + @Test + public void testHandleBulkDeleteTokensBatchException_genericException() { + RuntimeException ex = new RuntimeException("boom"); + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("t1")) + .build(); + + List errors = Utils.handleBulkDeleteTokensBatchException(ex, batch, 0, 50); + + Assert.assertEquals(1, errors.size()); + Assert.assertEquals(Integer.valueOf(500), errors.get(0).getHttpCode()); + Assert.assertEquals("t1", errors.get(0).getToken()); + } + + @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 testHandleBulkDeleteTokensBatchException_errorFieldNeitherMapNorStringUsesApiMessage() { + Map body = new HashMap<>(); + body.put("error", 500); + ApiClientApiException apiEx = new ApiClientApiException("delete failed", 500, 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()); + } + + @Test + 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") + .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( + wrapper, tokenizeBatch("v1", "group1"), 0); + + Assert.assertEquals(1, errors.size()); + 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 ─────────────────────────────────────────────── + + @Test + public void testFormatBulkInsertResponse_success() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder() + .skyflowId("sky-id-1") + .tokens(tokens) + .build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 0, 50, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + BulkInsertResponseRecord inserted = result.getRecords().get(0); + Assert.assertEquals("sky-id-1", inserted.getSkyflowId()); + Assert.assertEquals(tokens, inserted.getFields()); + Assert.assertEquals(0, inserted.getIndex()); + Assert.assertEquals(200, inserted.getHttpCode()); + Assert.assertNull(inserted.getError()); + } + + @Test + public void testFormatBulkInsertResponse_indexOffsetByBatchNumber() { + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 2, 50, new HashMap<>()); + + Assert.assertEquals(100, result.getRecords().get(0).getIndex()); + } + + @Test + public void testFormatBulkInsertResponse_errorWithMissingHttpCodeDefaultsTo500() { + V1RecordResponseObject record = V1RecordResponseObject.builder() + .error("insert failed") + .build(); + V1InsertResponse response = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + + BulkInsertResponse result = Utils.formatBulkInsertResponse(response, 0, 50, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(500, result.getRecords().get(0).getHttpCode()); + Assert.assertEquals("insert failed", result.getRecords().get(0).getError()); + } + + @Test + public void testFormatBulkInsertResponse_nullResponseReturnsNull() { + Assert.assertNull(Utils.formatBulkInsertResponse(null, 0, 50, new HashMap<>())); + } + + // ── formatBulkDetokenizeResponse ─────────────────────────────────────────── + + @Test + public void testFormatBulkDetokenizeResponse_success() { + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1") + .value("secret-value") + .build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)) + .build(); + + BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 0, 50, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + BulkDetokenizeResponseRecord detokenized = result.getRecords().get(0); + Assert.assertEquals("token1", detokenized.getToken()); + Assert.assertEquals(0, detokenized.getIndex()); + // Success records default to httpCode 200 and carry no error. + Assert.assertEquals(200, detokenized.getHttpCode()); + Assert.assertNull(detokenized.getError()); + // Per-batch responses leave the summary unset. + Assert.assertNull(result.getSummary()); + } + + @Test + public void testFormatBulkDetokenizeResponse_indexOffsetByBatchNumber() { + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1") + .build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)) + .build(); + + BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 2, 50, new HashMap<>()); + + Assert.assertEquals(100, result.getRecords().get(0).getIndex()); + } + + @Test + public void testFormatBulkDetokenizeResponse_errorWithMissingHttpCodeDefaultsTo500() { + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1") + .error("token not found") + .build(); + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)) + .build(); + + BulkDetokenizeResponse result = Utils.formatBulkDetokenizeResponse(response, 0, 50, new HashMap<>()); + + Assert.assertEquals(1, result.getRecords().size()); + Assert.assertEquals(500, result.getRecords().get(0).getHttpCode()); + Assert.assertEquals("token not found", result.getRecords().get(0).getError()); + } + + @Test + public void testFormatBulkDetokenizeResponse_emptyResponseReturnsNull() { + V1FlowDetokenizeResponse response = V1FlowDetokenizeResponse.builder().build(); + Assert.assertNull(Utils.formatBulkDetokenizeResponse(response, 0, 50, new HashMap<>())); + } + + // ── 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() + .value("token1") + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(record)) + .build(); + + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); + + 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 + public void testFormatBulkDeleteTokensResponse_error() { + V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() + .error("token not found") + .httpCode(404) + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(record)) + .build(); + + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); + + 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 + public void testFormatBulkDeleteTokensResponse_errorTextWithoutHttpCodeTreatedAsSuccess() { + V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder() + .value("token1") + .error("transient warning") + .build(); + V1FlowDeleteTokenResponse response = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(record)) + .build(); + + BulkDeleteTokensResponse result = Utils.formatBulkDeleteTokensResponse( + response, deleteBatchOf("token1"), 0, 50, new HashMap<>()); + + 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, 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() { + V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() + .value("value1") + .tokens(Collections.singletonList(FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").token("tok-abc").build())) + .build()); + + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + response, tokenizeBatch("value1", "group1"), 0, new HashMap<>()); + + 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() { + V1FlowTokenizeResponse response = tokenizeWire(V1FlowTokenizeResponseObject.builder() + .value("value1") + .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()); + + // this batch starts at index 40 in the caller's list + BulkTokenizeResponse result = Utils.formatBulkTokenizeResponse( + response, tokenizeBatch("value1", "group1"), 40, new HashMap<>()); + + Assert.assertEquals(40, result.getRecords().get(0).getIndex()); + } + + @Test + public void testFormatBulkTokenizeResponse_emptyResponseReturnsNull() { + Assert.assertNull(Utils.formatBulkTokenizeResponse( + V1FlowTokenizeResponse.builder().build(), + tokenizeBatch("value1", "group1"), 0, new HashMap<>())); + } + + // Tests for getQueryRequestBody / buildQueryResponse / getGetRequestBody / buildGetResponse + // were removed: get and query Utils helpers no longer exist (bulk-only module). + + // ── deleteTokens error records must survive any JSON number type ────────── + // recordMap holds deserialised JSON: Gson gives Double for numbers bound to Object, Jackson + // gives Integer or Long by magnitude. A blind (Integer) cast turned a real API error into a + // ClassCastException, so each representation is covered here. + + private static BulkDeleteTokensResponseRecord deleteError(Object httpCode) { + Map recordMap = new HashMap<>(); + if (httpCode != null) { + recordMap.put("http_code", httpCode); + } + recordMap.put("error", "Token not found"); + recordMap.put("value", "tok-1"); + Map body = new HashMap<>(); + body.put("tokens", Collections.singletonList(recordMap)); + V1FlowDeleteTokenRequest batch = V1FlowDeleteTokenRequest.builder() + .vaultId("vault123") + .tokens(Collections.singletonList("tok-1")) + .build(); + List records = Utils.handleBulkDeleteTokensBatchException( + new RuntimeException(new ApiClientApiException("delete failed", 500, body)), + batch, 0, 50); + return records.get(0); + } + + @Test + public void testDeleteTokensErrorRecord_acceptsIntegerHttpCode() { + Assert.assertEquals(Integer.valueOf(404), deleteError(404).getHttpCode()); + } + + @Test + public void testDeleteTokensErrorRecord_acceptsDoubleHttpCode() { + // Gson maps a JSON number to Double when the target type is Object. + Assert.assertEquals(Integer.valueOf(404), deleteError(404.0d).getHttpCode()); + } + + @Test + public void testDeleteTokensErrorRecord_acceptsLongHttpCode() { + Assert.assertEquals(Integer.valueOf(404), deleteError(404L).getHttpCode()); + } + + @Test + public void testDeleteTokensErrorRecord_acceptsStringHttpCode() { + Assert.assertEquals(Integer.valueOf(404), deleteError("404").getHttpCode()); + } + + @Test + public void testDeleteTokensErrorRecord_fallsBackTo500WhenTheCodeIsUnusable() { + Assert.assertEquals(Integer.valueOf(500), deleteError("not-a-number").getHttpCode()); + Assert.assertEquals(Integer.valueOf(500), deleteError(null).getHttpCode()); + } + + @Test + public void testDeleteTokensErrorRecord_keepsTheErrorAndEchoedToken() { + BulkDeleteTokensResponseRecord record = deleteError(404); + Assert.assertEquals("Token not found", record.getError()); + Assert.assertEquals("tok-1", record.getToken()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java new file mode 100644 index 00000000..7c20526a --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/utils/validations/ValidationsTests.java @@ -0,0 +1,1603 @@ +package com.skyflow.utils.validations; + +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.errors.ErrorMessage; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.BulkDeleteTokensRequest; +import com.skyflow.vault.data.BulkInsertRequestRecord; +import com.skyflow.vault.data.BulkInsertRequest; +import com.skyflow.vault.data.BulkDetokenizeRequest; +import com.skyflow.vault.data.BulkTokenizeRequestRecord; +import com.skyflow.vault.data.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; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ValidationsTests { + private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + + // Tests for validateTokenizeRequest / validateDeleteTokensRequest were removed: + // those unary validators no longer exist (bulk-only module). + + // ── validateCredentials ─────────────────────────────────────────────────── + + @Test + public void testValidateCredentials_validToken() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + try { + Validations.validateCredentials(credentials); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateCredentials_validApiKey() { + Credentials credentials = new Credentials(); + credentials.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + try { + Validations.validateCredentials(credentials); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateCredentials_invalidApiKeyFormat() { + Credentials credentials = new Credentials(); + credentials.setApiKey("not-a-valid-api-key"); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_noAuthMeansPassed() { + Credentials credentials = new Credentials(); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_multipleAuthMeansPassed() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + credentials.setApiKey("sky-ab123-abcd1234cdef1234abcd4321cdef4321"); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyRoles() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + credentials.setRoles(new ArrayList<>()); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_blankEntryInRoles() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + ArrayList roles = new ArrayList<>(); + roles.add("validRole"); + roles.add(" "); + credentials.setRoles(roles); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyPath() { + Credentials credentials = new Credentials(); + credentials.setPath(""); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyCredentialsString() { + Credentials credentials = new Credentials(); + credentials.setCredentialsString(""); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyToken() { + Credentials credentials = new Credentials(); + credentials.setToken(""); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyStringContext() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + credentials.setContext(""); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_emptyMapContext() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + credentials.setContext(new HashMap<>()); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_invalidMapKeyContext() { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + Map context = new HashMap<>(); + context.put("invalid-key!", "value"); + credentials.setContext(context); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateCredentials_invalidContextType() throws Exception { + Credentials credentials = new Credentials(); + credentials.setToken("some-token"); + Field contextField = credentials.getClass().getSuperclass().getDeclaredField("context"); + contextField.setAccessible(true); + contextField.set(credentials, Integer.valueOf(5)); + try { + Validations.validateCredentials(credentials); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── validateVaultConfiguration ──────────────────────────────────────────── + + @Test + public void testValidateVaultConfiguration_nullVaultId() { + VaultConfig config = new VaultConfig(); + config.setClusterId("cluster1"); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_emptyVaultId() { + VaultConfig config = new VaultConfig(); + config.setVaultId(" "); + config.setClusterId("cluster1"); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_neitherVaultUrlNorClusterId() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_emptyClusterId() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId(" "); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_emptyVaultUrl() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster1"); + config.setVaultUrl(" "); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_invalidVaultUrlFormat() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setVaultUrl("http://not-https.example.com"); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateVaultConfiguration_validWithClusterId() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster1"); + config.setEnv(Env.DEV); + try { + Validations.validateVaultConfiguration(config); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateVaultConfiguration_validWithVaultUrl() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setVaultUrl("https://myvault.example.com"); + try { + Validations.validateVaultConfiguration(config); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateVaultConfiguration_delegatesToCredentialValidation() { + VaultConfig config = new VaultConfig(); + config.setVaultId("vault123"); + config.setClusterId("cluster1"); + Credentials credentials = new Credentials(); + credentials.setApiKey("not-a-valid-api-key"); + config.setCredentials(credentials); + try { + Validations.validateVaultConfiguration(config); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // ── validateInsertRequest ───────────────────────────────────────────────── + + @Test + public void testValidateInsertRequest_nullRequest() { + try { + Validations.validateInsertRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_nullRecords() { + InsertRequest request = InsertRequest.builder().records(null).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_emptyRecords() { + InsertRequest request = InsertRequest.builder().records(new ArrayList<>()).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_nullRecordInList() { + ArrayList records = new ArrayList<>(); + records.add(null); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // NOTE: a table name may now be supplied at either the record level or the request level. + // EmptyTable is thrown only when it is missing from both. Specifying it at both levels is + // currently accepted (no conflict error) — that decision is deliberately deferred. Likewise + // the old "upsert present at record/request level requires table at the other level" checks are + // gone, since upsert is no longer coupled to where the table is specified — it's just an + // UpsertOptions object validated the same way at either level (see the upsert tests below). + + @Test + public void testValidateInsertRequest_missingTableNameInRecordThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_emptyTableNameInRecordThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName(" ").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_requestLevelTableNameSatisfiesRecordsWithoutOne() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_emptyUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build(); + InsertRequest request = InsertRequest.builder() + .records(records) + .upsert(upsert) + .build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_emptyUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).upsert(upsert).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_emptyOrNullKeyInData() { + Map data = new HashMap<>(); + data.put("", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_emptyOrNullValueInData() { + Map data = new HashMap<>(); + data.put("name", ""); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_validRequest() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("UPDATE") + .build(); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(upsert) + .build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("REPLACE") + .build(); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).upsert(upsert).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_validRequestWithTokens() { + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_emptyTokensMapThrows() { + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(new HashMap<>()).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_nullKeyInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put(null, "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyKeyInTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_blankKeyInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put(" ", "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyKeyInTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_nullValueInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put("name", null); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_blankValueInTokensThrows() { + Map tokens = new HashMap<>(); + tokens.put("name", " "); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").tokens(tokens).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); + } + } + + // ── validateDetokenizeRequest ───────────────────────────────────────────── + + @Test + public void testValidateDetokenizeRequest_nullRequest() { + try { + Validations.validateDetokenizeRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_nullTokens() { + DetokenizeRequest request = DetokenizeRequest.builder().tokens(null).build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_emptyTokens() { + DetokenizeRequest request = DetokenizeRequest.builder().tokens(new ArrayList<>()).build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_nullTokenInList() { + List tokens = new ArrayList<>(); + tokens.add(null); + DetokenizeRequest request = DetokenizeRequest.builder().tokens(tokens).build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_blankTokenInList() { + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList(" ")) + .build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_nullTokenGroupRedactionInList() { + List groupRedactions = new ArrayList<>(); + groupRedactions.add(null); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_blankTokenGroupNameInRedaction() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName(" ").redaction("MASKED").build()); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_blankRedactionInGroup() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction(" ").build()); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateDetokenizeRequest_validRequest() { + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + try { + Validations.validateDetokenizeRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateDetokenizeRequest_validRequestWithTokenGroupRedactions() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateDetokenizeRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateBulkInsertRequest ────────────────────────────────────────────── + + @Test + public void testValidateBulkInsertRequest_nullRequest() { + try { + Validations.validateBulkInsertRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_nullRecords() { + BulkInsertRequest request = BulkInsertRequest.builder().records(null).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_emptyRecords() { + BulkInsertRequest request = BulkInsertRequest.builder().records(new ArrayList<>()).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_nullRecordInList() { + ArrayList records = new ArrayList<>(); + records.add(null); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + // Removed: testValidateBulkInsertRequest_tableSpecifiedAtBothPlaces — bulk insert now + // delegates to validateInsertRequest, which deliberately accepts a table name at both + // the request and record level (TableSpecifiedInRequestAndRecordObject is no longer thrown). + + // Removed: testValidateBulkInsertRequest_upsertAtRecordLevelWhenTableAtRequestLevel and + // testValidateBulkInsertRequest_upsertAtRequestLevelWhenNoTableAtRequestLevel — upsert is + // now an UpsertOptions object accepted at either level, so UpsertTableRequestAtRecordLevel / + // UpsertTableRequestAtRequestLevel are no longer thrown for insert. + + @Test + public void testValidateBulkInsertRequest_tableNotSpecifiedAnywhereThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_requestLevelTableNameSatisfiesRecordsWithoutOne() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_tableNameAtBothLevelsThrows() { + // Table name must live at exactly one level, never both. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_emptyUpsertAtRecordLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().updateType("UPDATE").build()) + .build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyUpsertValues.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_emptyUpsertAtRequestLevel() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder() + .records(records) + .upsert(UpsertOptions.builder().uniqueColumns(new ArrayList<>()).build()) + .build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.EmptyUpsertValues.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_emptyKeyInData() { + Map data = new HashMap<>(); + data.put("", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_emptyValueInData() { + Map data = new HashMap<>(); + data.put("name", ""); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_validRequest() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_validRequestWithUpsertAndTokens() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder() + .tableName("table1") + .data(data) + .tokens(tokens) + .upsert(UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Collections.singletonList("email")) + .build()) + .build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_over10000RecordsThrows() { + // Constants.MAX_BULK_DATA_SIZE is a hard ceiling; batching splits the payload but + // does not lift it. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.RecordSizeExceedError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_exactly10000RecordsIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10000; i++) { + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_over10000TokensThrows() { + List tokens = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + tokens.add("token-" + i); + } + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TokensSizeExceedError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkDeleteTokensRequest_over10000TokensThrows() { + List tokens = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + tokens.add("token-" + i); + } + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + try { + Validations.validateBulkDeleteTokensRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.DeleteTokensSizeExceedError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_over10000RecordsThrows() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 10001; i++) { + records.add(BulkTokenizeRequestRecord.builder() + .value("value-" + i) + .tokenGroupNames(Collections.singletonList("group")) + .build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); + try { + Validations.validateBulkTokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TokenizeDataSizeExceedError.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_unrecognizedUpdateTypeThrows() { + // Previously this was silently dropped during mapping; it is now rejected up front. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("MERGE") + .build()) + .build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_updateTypeWithStrayWhitespaceThrows() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder() + .uniqueColumns(Collections.singletonList("email")) + .updateType("update ") + .build()) + .build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.InvalidUpsertUpdateType.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_nullUpdateTypeIsValid() { + // updateType is optional; only a non-null unrecognized value is rejected. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder() + .tableName("table1") + .records(records) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_plainInsertRequestRecordRejected() { + // `records` is inherited as List, so a plain unary record is + // accepted by the compiler. Bulk must reject it, otherwise getRecordsToRetry() would + // hit a ClassCastException later. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.InvalidRecord.getMessage(), e.getMessage()); + } + } + + // ── validateBulkInsertRequest: table-name presence rule ──────────────────── + // + // The rule is: a table name must be present at the request level OR on EVERY record. + // "Present" means non-null and non-blank at both levels (Validations trims before testing, + // and Utils.hasText applies the same test when mapping to the wire), so a blank record-level + // name is treated as absent and satisfied by the request-level one. + + @Test + public void testValidateBulkInsertRequest_requestLevelTableNameSatisfiesBlankRecordTableNames() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName(" ").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("").data(data).build()); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName("cards").records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_noRequestLevelTableNameButEveryRecordHasOneIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("accounts").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("people").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkInsertRequest_noRequestLevelTableNameAndOneRecordMissingThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName("people").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_noRequestLevelTableNameAndOneRecordBlankThrowsEmptyTable() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("cards").data(data).build()); + records.add(BulkInsertRequestRecord.builder().tableName(" ").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateBulkInsertRequest_blankRequestLevelTableNameDoesNotSatisfyRecords() { + // A blank request-level table name is treated as absent too, so records must supply one. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().tableName(" ").records(records).build(); + try { + Validations.validateBulkInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.TableNotSpecifiedInRequestAndRecordObject.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_upsertAtRequestLevelWithTableAtRecordLevelThrows() { + // upsert must sit at the same level as the table name. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder() + .records(records) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.UpsertTableRequestAtRequestLevel.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_upsertAtRecordLevelWithTableAtRequestLevelThrows() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder() + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateInsertRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorMessage.UpsertTableRequestAtRecordLevel.getMessage(), e.getMessage()); + } + } + + @Test + public void testValidateInsertRequest_upsertOnSomeRecordsOnlyIsValid() { + // Upsert is optional per record; it only has to sit at the same level as the table name. + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + records.add(InsertRequestRecord.builder().tableName("table1").data(data).build()); + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_upsertOnEveryRecordWithTableOnEveryRecordIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + records.add(InsertRequestRecord.builder() + .tableName("table1") + .data(data) + .upsert(UpsertOptions.builder().uniqueColumns(Collections.singletonList("email")).build()) + .build()); + } + InsertRequest request = InsertRequest.builder().records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateInsertRequest_noUpsertAnywhereIsValid() { + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(InsertRequestRecord.builder().data(data).build()); + InsertRequest request = InsertRequest.builder().tableName("table1").records(records).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateBulkDetokenizeRequest ────────────────────────────────────────── + + @Test + public void testValidateBulkDetokenizeRequest_nullRequest() { + try { + Validations.validateBulkDetokenizeRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_nullTokens() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(null).build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_emptyTokens() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(new ArrayList<>()).build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_nullTokenInList() { + List tokens = new ArrayList<>(); + tokens.add(null); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_nullRedactionGroupObject() { + List groupRedactions = new ArrayList<>(); + groupRedactions.add(null); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_nullGroupNameInRedaction() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().redaction("MASKED").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_nullRedactionInGroup() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateBulkDetokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_validRequest() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + try { + Validations.validateBulkDetokenizeRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkDetokenizeRequest_validRequestWithRedactions() { + List groupRedactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASKED").build()); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .tokenGroupRedactions(groupRedactions) + .build(); + try { + Validations.validateBulkDetokenizeRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateBulkDeleteTokensRequest ──────────────────────────────────────── + + @Test + public void testValidateBulkDeleteTokensRequest_nullRequest() { + try { + Validations.validateBulkDeleteTokensRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDeleteTokensRequest_emptyTokens() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(new ArrayList<>()).build(); + try { + Validations.validateBulkDeleteTokensRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDeleteTokensRequest_emptyTokenInList() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Arrays.asList("token1", " ")) + .build(); + try { + Validations.validateBulkDeleteTokensRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkDeleteTokensRequest_validRequest() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + try { + Validations.validateBulkDeleteTokensRequest(request); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // ── validateBulkTokenizeRequest ──────────────────────────────────────────── + + @Test + public void testValidateBulkTokenizeRequest_nullRequest() { + try { + Validations.validateBulkTokenizeRequest(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_emptyData() { + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(new ArrayList<>()).build(); + try { + Validations.validateBulkTokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_nullRecordInList() { + ArrayList data = new ArrayList<>(); + data.add(null); + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(data).build(); + try { + Validations.validateBulkTokenizeRequest(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_emptyValue() { + 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); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_emptyGroupNameInList() { + 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); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testValidateBulkTokenizeRequest_validRequestWithTokenGroupNames() { + 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) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testValidateBulkTokenizeRequest_validRequestWithoutTokenGroupNames() { + // Unlike the non-bulk tokenize validator, tokenGroupNames is optional for bulk requests. + 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) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + // Tests for validateQueryRequest / validateGetRequest were removed: + // those unary validators no longer exist (bulk-only module). + +} 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 new file mode 100644 index 00000000..72918e46 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -0,0 +1,1133 @@ +package com.skyflow.vault.controller; + +import com.skyflow.VaultClient; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.CustomHeaderKey; +import com.skyflow.enums.Env; +import com.skyflow.errors.SkyflowException; +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.core.RequestOptions; +import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient; +import com.skyflow.generated.rest.resources.flowservice.RawFlowserviceClient; +import com.skyflow.generated.rest.types.FlowTokenizeResponseObjectToken; +import com.skyflow.generated.rest.types.V1DeleteTokenResponseObject; +import com.skyflow.generated.rest.types.V1FlowDeleteTokenResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowDetokenizeResponseObject; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponse; +import com.skyflow.generated.rest.types.V1FlowTokenizeResponseObject; +import com.skyflow.generated.rest.types.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; +import com.skyflow.vault.data.BulkDetokenizeResponse; +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.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.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; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class VaultControllerTests { + private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + + private static VaultController createControllerWithMock(ApiClient mockApiClient) 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, mockApiClient); + return controller; + } + + private static Response buildOkHttpResponse() { + 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(); + } + + private static RawFlowserviceClient mockRawFlowservice(ApiClient mockApi) { + FlowserviceClient mockFlowservice = Mockito.mock(FlowserviceClient.class); + RawFlowserviceClient mockRaw = Mockito.mock(RawFlowserviceClient.class); + when(mockApi.flowservice()).thenReturn(mockFlowservice); + when(mockFlowservice.withRawResponse()).thenReturn(mockRaw); + return mockRaw; + } + + // Tests for the unary insert / detokenize / tokenize / deleteTokens controller methods + // (and their interceptor-header wiring) were removed: VaultController is bulk-only now. + + // ── bulkInsert ──────────────────────────────────────────────────────────── + + @Test + public void testBulkInsert_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-id-1", response.getRecords().get(0).getSkyflowId()); + Assert.assertNull(response.getRecords().get(0).getError()); + Assert.assertEquals(1, response.getSummary().getTotalRecords()); + Assert.assertEquals(1, response.getSummary().getTotalInserted()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); + } + + @Test + public void testBulkInsert_invalidRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + BulkInsertRequest request = BulkInsertRequest.builder().records(new ArrayList<>()).build(); + try { + controller.bulkInsert(request); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testBulkInsertAsync_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsertAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("sky-id-1", response.getRecords().get(0).getSkyflowId()); + } + + @Test + public void testBulkInsert_interceptorAddsCustomHeader() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + RequestInterceptor interceptor = ctx -> ctx.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "acct-123"); + BulkInsertOptions options = BulkInsertOptions.builder().interceptor(interceptor).build(); + + controller.bulkInsert(request, options); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RequestOptions.class); + Mockito.verify(mockRaw).insert(any(), captor.capture()); + Assert.assertEquals("acct-123", captor.getValue().getHeaders().get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID.toString())); + } + + // ── bulkDetokenize ──────────────────────────────────────────────────────── + + @Test + public void testBulkDetokenize_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1").value("secret-value").build(); + V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDetokenizeResponse response = controller.bulkDetokenize(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals("token1", response.getRecords().get(0).getToken()); + Assert.assertNull(response.getRecords().get(0).getError()); + Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); + } + + @Test + public void testBulkDetokenize_nullRequestThrowsSkyflowExceptionNotNPE() throws Exception { + // Regression test: configureDetokenizeConcurrencyAndBatchSize() must run AFTER validation, + // otherwise detokenizeRequest.getTokens().size() NPEs on a null request before validation + // has a chance to reject it gracefully. + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + try { + controller.bulkDetokenize(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } catch (NullPointerException e) { + Assert.fail("Expected SkyflowException, got NullPointerException"); + } + } + + @Test + public void testBulkDetokenizeAsync_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1FlowDetokenizeResponseObject record = V1FlowDetokenizeResponseObject.builder() + .token("token1").value("secret-value").build(); + V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() + .response(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.detokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDetokenizeResponse response = controller.bulkDetokenizeAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getError()); + } + + @Test + public void testBulkDetokenizeAsync_nullRequestThrowsSkyflowExceptionNotNPE() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + try { + controller.bulkDetokenizeAsync(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } catch (NullPointerException e) { + Assert.fail("Expected SkyflowException, got NullPointerException"); + } + } + + // ── bulkDeleteTokens ────────────────────────────────────────────────────── + + @Test + public void testBulkDeleteTokens_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder().value("token1").build(); + V1FlowDeleteTokenResponse body = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.deletetoken(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + 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 + public void testBulkDeleteTokens_nullRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + try { + controller.bulkDeleteTokens(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testBulkDeleteTokensAsync_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + V1DeleteTokenResponseObject record = V1DeleteTokenResponseObject.builder().value("token1").build(); + V1FlowDeleteTokenResponse body = V1FlowDeleteTokenResponse.builder() + .tokens(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.deletetoken(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokensAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getError()); + } + + // ── bulkTokenize ────────────────────────────────────────────────────────── + + @Test + public void testBulkTokenize_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").token("tok-abc").build(); + V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() + .value("value1").tokens(Collections.singletonList(token)).build(); + V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() + .response(Collections.singletonList(responseObject)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.tokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + 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.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 + public void testBulkTokenize_nullRequestThrowsSkyflowException() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + VaultController controller = createControllerWithMock(mockApi); + try { + controller.bulkTokenize(null); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertNotNull(e.getMessage()); + } + } + + @Test + public void testBulkTokenizeAsync_success() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1").token("tok-abc").build(); + V1FlowTokenizeResponseObject responseObject = V1FlowTokenizeResponseObject.builder() + .value("value1").tokens(Collections.singletonList(token)).build(); + V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder() + .response(Collections.singletonList(responseObject)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.tokenize(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + 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.getRecords().size()); + Assert.assertNull(response.getRecords().get(0).getTokens().get(0).getError()); + } + + // ── additional bulk API-error coverage ─────────────────────────────────── + // + // Note: unlike the singular insert/detokenize/tokenize/deleteTokens calls, the bulk* + // entry points recover a per-batch ApiClientApiException into the response's errors list + // (via Utils.handleBulkXxxBatchException, invoked from the CompletableFuture + // .exceptionally()/.handle() callbacks inside VaultController's *BatchFutures helpers) + // instead of letting a SkyflowException escape the call. That is a deliberate resilience + // design (a single failed batch shouldn't fail an entire bulk request), so these tests + // assert the errors-list outcome rather than a thrown exception. + + @Test + public void testBulkInsertAsync_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.insert(any(), any())) + .thenThrow(new ApiClientApiException("insert failed", 401, "unauthorized")); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsertAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(401, response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertNull(response.getRecords().get(0).getSkyflowId()); + } + + @Test + public void testBulkDeleteTokens_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.deletetoken(any(), any())) + .thenThrow(new ApiClientApiException("delete failed", 404, "not found")); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + 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 + public void testBulkDeleteTokensAsync_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.deletetoken(any(), any())) + .thenThrow(new ApiClientApiException("delete failed", 404, "not found")); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokensAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + 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 + public void testBulkTokenize_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.tokenize(any(), any())) + .thenThrow(new ApiClientApiException("tokenize failed", 400, "bad request")); + + VaultController controller = createControllerWithMock(mockApi); + + 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.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 + public void testBulkTokenizeAsync_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.tokenize(any(), any())) + .thenThrow(new ApiClientApiException("tokenize failed", 400, "bad request")); + + VaultController controller = createControllerWithMock(mockApi); + + 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.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 + public void testBulkDetokenizeAsync_apiErrorCapturedInErrors() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + when(mockRaw.detokenize(any(), any())) + .thenThrow(new ApiClientApiException("detokenize failed", 401, "unauthorized")); + + VaultController controller = createControllerWithMock(mockApi); + + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("token1")) + .build(); + + BulkDetokenizeResponse response = controller.bulkDetokenizeAsync(request).get(5, TimeUnit.SECONDS); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + Assert.assertEquals(401, response.getRecords().get(0).getHttpCode()); + Assert.assertNotNull(response.getRecords().get(0).getError()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + Assert.assertEquals(0, response.getSummary().getTotalDetokenized()); + } + + // ── multi-batch aggregation / partial-batch failure ────────────────────── + + @Test + public void testBulkInsert_multiBatchAggregatesAcrossBatches() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens1 = new HashMap<>(); + tokens1.put("name", "tok-batch1"); + V1RecordResponseObject record1 = V1RecordResponseObject.builder().skyflowId("sky-id-batch1").tokens(tokens1).build(); + V1InsertResponse body1 = V1InsertResponse.builder().records(Collections.singletonList(record1)).build(); + ApiClientHttpResponse httpResp1 = new ApiClientHttpResponse<>(body1, buildOkHttpResponse()); + + Map tokens2 = new HashMap<>(); + tokens2.put("name", "tok-batch2"); + V1RecordResponseObject record2 = V1RecordResponseObject.builder().skyflowId("sky-id-batch2").tokens(tokens2).build(); + V1InsertResponse body2 = V1InsertResponse.builder().records(Collections.singletonList(record2)).build(); + ApiClientHttpResponse httpResp2 = new ApiClientHttpResponse<>(body2, buildOkHttpResponse()); + + when(mockRaw.insert(any(), any())).thenReturn(httpResp1).thenReturn(httpResp2); + + VaultController controller = createControllerWithMock(mockApi); + + // Constants.INSERT_BATCH_SIZE defaults to 50, so 75 records forces exactly two batches + // (50 + 25) — processBulkInsertSync/insertBatchFutures must merge the per-batch record + // lists collected from more than one CompletableFuture into a single BulkInsertResponse. + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 75; i++) { + Map data = new HashMap<>(); + data.put("name", "john" + i); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Mockito.verify(mockRaw, Mockito.times(2)).insert(any(), any()); + Assert.assertEquals(2, response.getRecords().size()); + Assert.assertTrue(response.getRecords().stream().allMatch(r -> r.getError() == null)); + Assert.assertEquals(75, response.getSummary().getTotalRecords()); + Assert.assertEquals(2, response.getSummary().getTotalInserted()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); + + BulkInsertResponseRecord batch1Success = response.getRecords().stream() + .filter(s -> "sky-id-batch1".equals(s.getSkyflowId())).findFirst().orElse(null); + BulkInsertResponseRecord batch2Success = response.getRecords().stream() + .filter(s -> "sky-id-batch2".equals(s.getSkyflowId())).findFirst().orElse(null); + Assert.assertNotNull(batch1Success); + Assert.assertNotNull(batch2Success); + // Index offsets prove the batch-number * batchSize aggregation math in + // Utils.formatBulkInsertResponse: batch 0 starts at index 0, batch 1 at index 50. + Assert.assertEquals(0, batch1Success.getIndex()); + Assert.assertEquals(50, batch2Success.getIndex()); + } + + @Test + public void testBulkInsert_partialBatchFailureKeepsBatch1SuccessAndBatch2Error() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokens1 = new HashMap<>(); + tokens1.put("name", "tok-batch1"); + V1RecordResponseObject record1 = V1RecordResponseObject.builder().skyflowId("sky-id-batch1").tokens(tokens1).build(); + V1InsertResponse body1 = V1InsertResponse.builder().records(Collections.singletonList(record1)).build(); + ApiClientHttpResponse httpResp1 = new ApiClientHttpResponse<>(body1, buildOkHttpResponse()); + + when(mockRaw.insert(any(), any())) + .thenReturn(httpResp1) + .thenThrow(new ApiClientApiException("insert failed", 500, "server error")); + + VaultController controller = createControllerWithMock(mockApi); + + // 51 records -> batch 1 has 50 records (succeeds), batch 2 has 1 record (fails). + ArrayList records = new ArrayList<>(); + for (int i = 0; i < 51; i++) { + Map data = new HashMap<>(); + data.put("name", "john" + i); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + } + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Mockito.verify(mockRaw, Mockito.times(2)).insert(any(), any()); + + Assert.assertEquals(2, response.getRecords().size()); + + BulkInsertResponseRecord inserted = response.getRecords().stream() + .filter(r -> r.getError() == null).findFirst().orElse(null); + Assert.assertNotNull(inserted); + Assert.assertEquals("sky-id-batch1", inserted.getSkyflowId()); + Assert.assertEquals(0, inserted.getIndex()); + + BulkInsertResponseRecord failed = response.getRecords().stream() + .filter(r -> r.getError() != null).findFirst().orElse(null); + Assert.assertNotNull(failed); + Assert.assertEquals(50, failed.getIndex()); + Assert.assertEquals(500, failed.getHttpCode()); + + Assert.assertEquals(51, response.getSummary().getTotalRecords()); + Assert.assertEquals(1, response.getSummary().getTotalInserted()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + } + + // ── formatBulkInsertResponse: List token shape ────────────────────── + + @Test + public void testBulkInsert_successWithListOfMapsTokenShape() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + + Map tokenEntry = new HashMap<>(); + tokenEntry.put("token", "tok-xyz"); + tokenEntry.put("tokenGroupName", "group1"); + List> tokenList = new ArrayList<>(); + tokenList.add(tokenEntry); + + Map tokens = new HashMap<>(); + tokens.put("field1", tokenList); + + V1RecordResponseObject record = V1RecordResponseObject.builder().skyflowId("sky-id-1").tokens(tokens).build(); + V1InsertResponse body = V1InsertResponse.builder().records(Collections.singletonList(record)).build(); + ApiClientHttpResponse httpResp = new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + when(mockRaw.insert(any(), any())).thenReturn(httpResp); + + VaultController controller = createControllerWithMock(mockApi); + + Map data = new HashMap<>(); + data.put("name", "john"); + ArrayList records = new ArrayList<>(); + records.add(BulkInsertRequestRecord.builder().tableName("table1").data(data).build()); + BulkInsertRequest request = BulkInsertRequest.builder().records(records).build(); + + BulkInsertResponse response = controller.bulkInsert(request); + Assert.assertNotNull(INVALID_EXCEPTION_THROWN, response); + Assert.assertEquals(1, response.getRecords().size()); + + BulkInsertResponseRecord inserted = response.getRecords().get(0); + Assert.assertNotNull(inserted.getFields()); + // The token map is surfaced verbatim as `fields`, so a List token shape survives intact. + Object field1Tokens = inserted.getFields().get("field1"); + Assert.assertTrue(field1Tokens instanceof List); + Assert.assertEquals(1, ((List) field1Tokens).size()); + Map field1Token = (Map) ((List) field1Tokens).get(0); + Assert.assertEquals("tok-xyz", field1Token.get("token")); + Assert.assertEquals("group1", field1Token.get("tokenGroupName")); + } + + // Tests for the unary query / get controller methods were removed: VaultController is bulk-only now. + + // ───────────────────────────────────────────────────────────────────────── + // Request fidelity through batch dispatch + // + // Constants.INSERT/DETOKENIZE/TOKENIZE/DELETE_TOKENS_BATCH_SIZE all default to 50, so a + // 120-item request produces exactly three batches (50 + 50 + 20). These tests assert that + // (a) non-batched fields are re-applied to EVERY outgoing batch request, (b) item order is + // preserved end to end, (c) the SDK-assigned response index equals the item's position in + // the ORIGINAL user list, and (d) a registered interceptor runs once per batch. + // ───────────────────────────────────────────────────────────────────────── + + private static final int MULTI_BATCH_ITEM_COUNT = 120; + private static final int EXPECTED_BATCH_COUNT = 3; + + /** Interceptor that records each RequestContext it is handed and stamps a per-batch header. */ + private static final class CountingInterceptor implements RequestInterceptor { + private final List contexts = + java.util.Collections.synchronizedList(new ArrayList<>()); + + @Override + public void intercept(com.skyflow.vault.data.RequestContext context) { + int callNumber; + synchronized (contexts) { + callNumber = contexts.size(); + contexts.add(context); + } + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "batch-" + callNumber); + } + + int callCount() { + return contexts.size(); + } + + List contexts() { + return contexts; + } + } + + private static void assertInterceptorRanOncePerBatch(CountingInterceptor interceptor, + List capturedOptions) { + Assert.assertEquals(EXPECTED_BATCH_COUNT, interceptor.callCount()); + // Each batch must get its own RequestContext — never a shared/reused one. + java.util.Set identities = new java.util.HashSet<>(); + for (com.skyflow.vault.data.RequestContext ctx : interceptor.contexts()) { + identities.add(System.identityHashCode(ctx)); + } + Assert.assertEquals(EXPECTED_BATCH_COUNT, identities.size()); + + // Each context must also report where its batch sits in the request, so an interceptor can + // tag batches apart (per-batch correlation id, "batch 3 of 12" logging). Every index in + // 0..n-1 must appear exactly once, and every context must agree on the total. + java.util.Set batchIndexes = new java.util.HashSet<>(); + for (com.skyflow.vault.data.RequestContext ctx : interceptor.contexts()) { + Assert.assertEquals("totalBatches must be the real batch count", + EXPECTED_BATCH_COUNT, ctx.getTotalBatches()); + Assert.assertTrue("batchIndex out of range: " + ctx.getBatchIndex(), + ctx.getBatchIndex() >= 0 && ctx.getBatchIndex() < EXPECTED_BATCH_COUNT); + batchIndexes.add(ctx.getBatchIndex()); + } + Assert.assertEquals("every batch position must appear exactly once", + EXPECTED_BATCH_COUNT, batchIndexes.size()); + + // The header the interceptor set on each context must reach that batch's RequestOptions. + Assert.assertEquals(EXPECTED_BATCH_COUNT, capturedOptions.size()); + java.util.Set headerValues = new java.util.HashSet<>(); + for (RequestOptions options : capturedOptions) { + String value = options.getHeaders().get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID.toString()); + Assert.assertNotNull("Interceptor header missing on a batch", value); + headerValues.add(value); + } + Assert.assertEquals( + new java.util.HashSet<>(java.util.Arrays.asList("batch-0", "batch-1", "batch-2")), + headerValues); + } + + private static ArrayList multiBatchInsertRecords() { + ArrayList records = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Map data = new HashMap<>(); + data.put("pos", String.valueOf(i)); + records.add(BulkInsertRequestRecord.builder().data(data).build()); + } + return records; + } + + private static List multiBatchTokens() { + List tokens = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + tokens.add("token-" + i); + } + return tokens; + } + + /** Echoes each request record back as a response record whose skyflowId encodes its "pos". */ + private static void stubInsertEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.insert(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (com.skyflow.generated.rest.types.V1InsertRecordData record : req.getRecords().get()) { + responseRecords.add(V1RecordResponseObject.builder() + .skyflowId("sky-" + record.getData().get().get("pos")) + .tableName(record.getTableName().orElse(null)) + .build()); + } + V1InsertResponse body = V1InsertResponse.builder().records(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } + + /** Echoes each requested token back as a detokenize response record. */ + private static void stubDetokenizeEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.detokenize(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (String token : req.getTokens().get()) { + responseRecords.add(V1FlowDetokenizeResponseObject.builder().token(token).build()); + } + V1FlowDetokenizeResponse body = V1FlowDetokenizeResponse.builder() + .response(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } + + /** Echoes each requested token back as a delete-token response record. */ + private static void stubDeleteTokensEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.deletetoken(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (String token : req.getTokens().get()) { + responseRecords.add(V1DeleteTokenResponseObject.builder().value(token).build()); + } + V1FlowDeleteTokenResponse body = V1FlowDeleteTokenResponse.builder() + .tokens(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } + + /** Echoes each requested tokenize value back with one token per requested group name. */ + private static void stubTokenizeEcho(RawFlowserviceClient mockRaw) { + when(mockRaw.tokenize(any(), any())).thenAnswer(invocation -> { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest req = + invocation.getArgument(0); + List responseRecords = new ArrayList<>(); + for (com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj : req.getData().get()) { + FlowTokenizeResponseObjectToken token = FlowTokenizeResponseObjectToken.builder() + .tokenGroupName("group1") + .token("tok-" + obj.getValue().get()) + .build(); + responseRecords.add(V1FlowTokenizeResponseObject.builder() + .value(obj.getValue().get()) + .tokens(Collections.singletonList(token)) + .build()); + } + V1FlowTokenizeResponse body = V1FlowTokenizeResponse.builder().response(responseRecords).build(); + return new ApiClientHttpResponse<>(body, buildOkHttpResponse()); + }); + } + + // ── bulk insert: batch dispatch fidelity ───────────────────────────────── + + @Test + public void testBulkInsert_tableNameAndVaultIdReAppliedOnEveryBatch() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); + + controller.bulkInsert(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).insert(captor.capture(), any()); + + int expectedPos = 0; + for (com.skyflow.generated.rest.resources.flowservice.requests.V1InsertRequest sent : captor.getAllValues()) { + // insertBatch rebuilds the request per batch, so tableName/vaultId must be re-applied. + Assert.assertEquals("cards", sent.getTableName().get()); + Assert.assertEquals("vault123", sent.getVaultId().get()); + for (com.skyflow.generated.rest.types.V1InsertRecordData record : sent.getRecords().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++; + } + } + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, expectedPos); + } + + @Test + public void testBulkInsert_responseIndexMapsToOriginalInputPosition_acrossBatches() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); + + BulkInsertResponse response = controller.bulkInsert(request); + + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + BulkInsertResponseRecord record = response.getRecords().get(i); + Assert.assertEquals(i, record.getIndex()); + // skyflowId encodes the input record's position, so this proves index -> input position. + Assert.assertEquals("sky-" + i, record.getSkyflowId()); + } + } + + @Test + public void testBulkInsert_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubInsertEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("cards") + .records(multiBatchInsertRecords()) + .build(); + + CountingInterceptor interceptor = new CountingInterceptor(); + 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()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); + } + + // ── bulk detokenize: batch dispatch fidelity ───────────────────────────── + + @Test + public void testBulkDetokenize_vaultIdAndRedactionsReachEveryBatchAndTokenOrderPreserved() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDetokenizeEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + List tokens = multiBatchTokens(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group one").redaction("MASKED").build())) + .build(); + + controller.bulkDetokenize(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).detokenize(captor.capture(), any()); + + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + Assert.assertTrue(sent.getTokenGroupRedactions().isPresent()); + Assert.assertEquals("group one", sent.getTokenGroupRedactions().get().get(0).getTokenGroupName().get()); + Assert.assertEquals("MASKED", sent.getTokenGroupRedactions().get().get(0).getRedaction().get()); + flattened.addAll(sent.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + } + + @Test + public void testBulkDetokenize_responseIndexMapsToOriginalInputPosition_acrossBatches() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDetokenizeEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + List tokens = multiBatchTokens(); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(tokens).build(); + + BulkDetokenizeResponse response = controller.bulkDetokenize(request); + + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals(i, response.getRecords().get(i).getIndex()); + Assert.assertEquals(tokens.get(i), response.getRecords().get(i).getToken()); + } + } + + @Test + public void testBulkDetokenize_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDetokenizeEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().tokens(multiBatchTokens()).build(); + + CountingInterceptor interceptor = new CountingInterceptor(); + 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()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); + } + + // ── bulk delete tokens: batch dispatch fidelity ────────────────────────── + + @Test + public void testBulkDeleteTokens_vaultIdOnEveryBatchAndTokenOrderPreserved() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDeleteTokensEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + List tokens = multiBatchTokens(); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + + BulkDeleteTokensResponse response = controller.bulkDeleteTokens(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).deletetoken(captor.capture(), any()); + + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + flattened.addAll(sent.getTokens().get()); + } + Assert.assertEquals(tokens, flattened); + + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, response.getRecords().size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals(i, response.getRecords().get(i).getIndex()); + Assert.assertEquals(tokens.get(i), response.getRecords().get(i).getToken()); + } + } + + @Test + public void testBulkDeleteTokens_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubDeleteTokensEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(multiBatchTokens()).build(); + + CountingInterceptor interceptor = new CountingInterceptor(); + 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()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); + } + + // ── bulk tokenize: batch dispatch fidelity ─────────────────────────────── + + @Test + public void testBulkTokenize_vaultIdOnEveryBatchAndValueOrderPreserved() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubTokenizeEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + records.add(BulkTokenizeRequestRecord.builder() + .value("value-" + i) + .tokenGroupNames(Collections.singletonList("group1")) + .build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); + + controller.bulkTokenize(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.class); + Mockito.verify(mockRaw, Mockito.times(EXPECTED_BATCH_COUNT)).tokenize(captor.capture(), any()); + + List flattened = new ArrayList<>(); + for (com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest sent : captor.getAllValues()) { + Assert.assertEquals("vault123", sent.getVaultId().get()); + for (com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject obj : sent.getData().get()) { + Assert.assertEquals(Collections.singletonList("group1"), obj.getTokenGroupNames().get()); + flattened.add(obj.getValue().get()); + } + } + Assert.assertEquals(MULTI_BATCH_ITEM_COUNT, flattened.size()); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + Assert.assertEquals("value-" + i, flattened.get(i)); + } + } + + @Test + public void testBulkTokenize_interceptorInvokedOncePerBatchWithDistinctContext() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RawFlowserviceClient mockRaw = mockRawFlowservice(mockApi); + stubTokenizeEcho(mockRaw); + + VaultController controller = createControllerWithMock(mockApi); + ArrayList records = new ArrayList<>(); + for (int i = 0; i < MULTI_BATCH_ITEM_COUNT; i++) { + records.add(BulkTokenizeRequestRecord.builder().value("value-" + i).build()); + } + BulkTokenizeRequest request = BulkTokenizeRequest.builder().records(records).build(); + + CountingInterceptor interceptor = new CountingInterceptor(); + 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()); + assertInterceptorRanOncePerBatch(interceptor, captor.getAllValues()); + } +} diff --git a/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java new file mode 100644 index 00000000..fc6b4e65 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/BulkResponseTests.java @@ -0,0 +1,538 @@ +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.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests for the Bulk*Response classes: {@link BulkInsertResponse}, {@link BulkDetokenizeResponse}, + * {@link BulkDeleteTokensResponse} and {@link BulkTokenizeResponse}. + * + *

BulkDeleteTokensResponse/BulkTokenizeResponse each have a 2-arg constructor (success/errors + * only, summary and originalPayload-derived fields stay null) and a 3-arg constructor (adds + * originalPayload and computes a summary). BulkInsertResponse and BulkDetokenizeResponse instead + * carry a single unified {@code records} list, with a 1-arg (per-batch) and a 2-arg (final, + * summary-computing) constructor. Both additionally derive a retry list from the originalPayload, + * filtering on a "5xx except 529" retryable-status rule. + */ +public class BulkResponseTests { + + // ── BulkInsertResponse ─────────────────────────────────────────────────── + + @Test + public void testBulkInsertResponse_oneArgConstructorLeavesSummaryAndRetryDependenciesNull() { + List records = Collections.emptyList(); + + BulkInsertResponse response = new BulkInsertResponse(records); + + Assert.assertNull(response.getSummary()); + Assert.assertEquals(records, response.getRecords()); + // No failed records, so the retry list is empty and originalPayload (null) is never dereferenced. + Assert.assertTrue(response.getRecordsToRetry().isEmpty()); + } + + @Test + public void testBulkInsertResponse_twoArgConstructorComputesSummary() { + List records = Arrays.asList( + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null), + new BulkInsertResponseRecord(1, null, null, null, null, 400, "failed", null)); + List originalPayload = new ArrayList<>(Arrays.asList( + BulkInsertRequestRecord.builder().tableName("table1").build(), + BulkInsertRequestRecord.builder().tableName("table1").build())); + + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); + + Assert.assertNotNull(response.getSummary()); + Assert.assertEquals(2, response.getSummary().getTotalRecords()); + Assert.assertEquals(1, response.getSummary().getTotalInserted()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + } + + @Test + public void testBulkInsertResponse_recordsPreserveIndexAndInheritedFields() { + Map fields = new HashMap<>(); + fields.put("name", "token-name"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-name"); + + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 7, "table1", "id-1", fields, hashedData, 200, null, null); + + BulkInsertResponse response = new BulkInsertResponse(Collections.singletonList(record)); + + BulkInsertResponseRecord actual = response.getRecords().get(0); + Assert.assertEquals(7, actual.getIndex()); + Assert.assertEquals("table1", actual.getTableName()); + Assert.assertEquals("id-1", actual.getSkyflowId()); + Assert.assertEquals(fields, actual.getFields()); + Assert.assertEquals(hashedData, actual.getHashedData()); + Assert.assertEquals(200, actual.getHttpCode()); + Assert.assertNull(actual.getError()); + } + + @Test + public void testBulkInsertResponse_getRecordsToRetryFiltersRetryableStatusCodesOnly() { + BulkInsertRequestRecord record0 = BulkInsertRequestRecord.builder().tableName("t0").build(); + BulkInsertRequestRecord record1 = BulkInsertRequestRecord.builder().tableName("t1").build(); + BulkInsertRequestRecord record2 = BulkInsertRequestRecord.builder().tableName("t2").build(); + BulkInsertRequestRecord record3 = BulkInsertRequestRecord.builder().tableName("t3").build(); + List originalPayload = new ArrayList<>( + Arrays.asList(record0, record1, record2, record3)); + + List records = Arrays.asList( + new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null), // retryable (lower bound) + new BulkInsertResponseRecord(1, null, null, null, null, 400, "bad request", null), // not retryable + new BulkInsertResponseRecord(2, null, null, null, null, 599, "server error", null), // retryable (upper bound) + new BulkInsertResponseRecord(3, null, null, null, null, 529, "special case", null)); // explicitly excluded + + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); + + List recordsToRetry = response.getRecordsToRetry(); + + Assert.assertEquals(2, recordsToRetry.size()); + Assert.assertTrue(recordsToRetry.contains(record0)); + Assert.assertTrue(recordsToRetry.contains(record2)); + Assert.assertFalse(recordsToRetry.contains(record1)); + Assert.assertFalse(recordsToRetry.contains(record3)); + } + + @Test + public void testBulkInsertResponse_toStringNotNull() { + BulkInsertResponse response = new BulkInsertResponse(Collections.emptyList()); + Assert.assertNotNull(response.toString()); + } + + @Test + public void testBulkInsertResponse_toStringSerializesSummaryAndRecordsButNotInternals() { + List records = Collections.singletonList( + new BulkInsertResponseRecord(0, "table1", "id-1", null, null, 200, null, null)); + List originalPayload = new ArrayList( + Collections.singletonList(BulkInsertRequestRecord.builder().tableName("table1").build())); + + BulkInsertResponse response = new BulkInsertResponse(records, originalPayload); + // Populate the lazily-derived internal so we can prove it is still excluded. + response.getRecordsToRetry(); + String json = response.toString(); + + Assert.assertTrue(json.contains("summary")); + Assert.assertTrue(json.contains("records")); + // serializeNulls() spells out the nulls on each record. + Assert.assertTrue(json.contains("\"error\":null")); + // transient internals stay out of the JSON. + Assert.assertFalse(json.contains("originalPayload")); + Assert.assertFalse(json.contains("recordsToRetry")); + } + + @Test + public void testBulkInsertResponse_getRecordsToRetryOnPerBatchResponseDoesNotThrow() { + // The 1-arg constructor leaves originalPayload null. A 5xx record must not NPE here. + List records = Collections.singletonList( + new BulkInsertResponseRecord(0, null, null, null, null, 500, "server error", null)); + + BulkInsertResponse response = new BulkInsertResponse(records); + + Assert.assertTrue(response.getRecordsToRetry().isEmpty()); + } + + // ── BulkDetokenizeResponse ─────────────────────────────────────────────── + + @Test + public void testBulkDetokenizeResponse_oneArgConstructorLeavesSummaryAndRetryDependenciesNull() { + List records = Collections.emptyList(); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); + + Assert.assertNull(response.getSummary()); + Assert.assertEquals(records, response.getRecords()); + // No retryable records, so originalPayload (null) is never dereferenced. + Assert.assertTrue(response.getTokensToRetry().isEmpty()); + } + + @Test + public void testBulkDetokenizeResponse_twoArgConstructorComputesSummary() { + List records = Arrays.asList( + new BulkDetokenizeResponseRecord(0, "tok-1", "secret-value", "group1", null, 200, null, null), + new BulkDetokenizeResponseRecord(1, "tok-2", null, null, null, 404, "failed", null)); + List originalPayload = Arrays.asList("tok-1", "tok-2"); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + + Assert.assertNotNull(response.getSummary()); + Assert.assertEquals(2, response.getSummary().getTotalTokens()); + Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); + Assert.assertEquals(1, response.getSummary().getTotalFailed()); + Assert.assertEquals(records, response.getRecords()); + } + + @Test + public void testBulkDetokenizeResponse_summaryTotalTokensComesFromOriginalPayloadNotRecords() { + // Only one of the three submitted tokens came back, so totalTokens tracks the payload size. + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "plain-0", "group1", null, 200, null, null)); + List originalPayload = Arrays.asList("tok-0", "tok-1", "tok-2"); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + + Assert.assertEquals(3, response.getSummary().getTotalTokens()); + Assert.assertEquals(1, response.getSummary().getTotalDetokenized()); + Assert.assertEquals(0, response.getSummary().getTotalFailed()); + } + + @Test + public void testBulkDetokenizeResponse_getTokensToRetryFiltersRetryableStatusCodesOnly() { + List originalPayload = Arrays.asList("tok-0", "tok-1", "tok-2", "tok-3"); + List records = Arrays.asList( + new BulkDetokenizeResponseRecord(0, null, null, null, null, 500, "server error", null), + new BulkDetokenizeResponseRecord(1, null, null, null, null, 400, "bad request", null), + new BulkDetokenizeResponseRecord(2, null, null, null, null, 599, "server error", null), + new BulkDetokenizeResponseRecord(3, null, null, null, null, 529, "special case", null)); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + + 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")); + } + + @Test + public void testBulkDetokenizeResponse_toStringNotNull() { + BulkDetokenizeResponse response = new BulkDetokenizeResponse(Collections.emptyList()); + Assert.assertNotNull(response.toString()); + } + + @Test + public void testBulkDetokenizeResponse_toStringSerializesSummaryAndRecordsButNotInternals() { + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "plain-0", "group1", null, 200, null, null)); + List originalPayload = Collections.singletonList("tok-0"); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records, originalPayload); + // Populate the lazily-derived internal so we can prove it is still excluded. + response.getTokensToRetry(); + String json = response.toString(); + + Assert.assertTrue(json.contains("summary")); + Assert.assertTrue(json.contains("records")); + // serializeNulls() spells out the nulls on each record. + Assert.assertTrue(json.contains("\"error\":null")); + // transient internals stay out of the JSON. + Assert.assertFalse(json.contains("originalPayload")); + Assert.assertFalse(json.contains("tokensToRetry")); + } + + @Test + public void testBulkDetokenizeResponse_getTokensToRetryOnPerBatchResponseDoesNotThrow() { + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", null, null, null, 500, "server error", null)); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); + + Assert.assertTrue(response.getTokensToRetry().isEmpty()); + } + + @Test + public void testBulkDetokenizeResponse_recordsExposeDetokenizedValue() { + // value is passed through verbatim from the generated response object. + List records = Collections.singletonList( + new BulkDetokenizeResponseRecord(0, "tok-0", "john@example.com", "group1", null, 200, null, null)); + + BulkDetokenizeResponse response = new BulkDetokenizeResponse(records); + + Assert.assertEquals("john@example.com", response.getRecords().get(0).getValue()); + } + + // ── BulkDeleteTokensResponse ───────────────────────────────────────────── + + @Test + public void testBulkDeleteTokensResponse_singleArgConstructorLeavesSummaryNull() { + List records = Collections.emptyList(); + + BulkDeleteTokensResponse response = new BulkDeleteTokensResponse(records); + + Assert.assertNull(response.getSummary()); + Assert.assertEquals(records, response.getRecords()); + } + + @Test + 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(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_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_singleArgConstructorLeavesSummaryNull() { + List records = Collections.emptyList(); + + BulkTokenizeResponse response = new BulkTokenizeResponse(records); + + Assert.assertNull(response.getSummary()); + Assert.assertEquals(records, response.getRecords()); + } + + @Test + 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(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()); + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + 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 new file mode 100644 index 00000000..83b54775 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/OptionsTests.java @@ -0,0 +1,159 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; +public class OptionsTests { + + private static final RequestInterceptor INTERCEPTOR = context -> { + }; + + // ── InsertOptions ──────────────────────────────────────────────────────── + + @Test + public void testInsertOptions_withInterceptor() { + InsertOptions options = InsertOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + public void testInsertOptions_withoutInterceptor() { + InsertOptions options = InsertOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + // ── DetokenizeOptions ──────────────────────────────────────────────────── + + @Test + public void testDetokenizeOptions_withInterceptor() { + DetokenizeOptions options = DetokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + public void testDetokenizeOptions_withoutInterceptor() { + DetokenizeOptions options = DetokenizeOptions.builder().build(); + 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 + public void testTokenizeOptions_withInterceptor() { + TokenizeOptions options = TokenizeOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + public void testTokenizeOptions_withoutInterceptor() { + TokenizeOptions options = TokenizeOptions.builder().build(); + Assert.assertNull(options.getInterceptor()); + } + + // ── DeleteTokensOptions ────────────────────────────────────────────────── + + @Test + public void testDeleteTokensOptions_withInterceptor() { + DeleteTokensOptions options = DeleteTokensOptions.builder().interceptor(INTERCEPTOR).build(); + Assert.assertSame(INTERCEPTOR, options.getInterceptor()); + } + + @Test + 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 new file mode 100644 index 00000000..52cbf10c --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/RecordAndRedactionTests.java @@ -0,0 +1,165 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests for the plain record/redaction data holders: {@link TokenGroupRedactions}, + * {@link InsertRequestRecord}, {@link BulkInsertRequestRecord} + * and {@link BulkTokenizeRequestRecord}. None of these classes perform + * validation in their builders, so coverage here is builder-construction plus getters. + */ +public class RecordAndRedactionTests { + + // ── TokenGroupRedactions ───────────────────────────────────────────────── + + @Test + public void testTokenGroupRedactions_gettersReturnBuilderValues() { + TokenGroupRedactions redaction = TokenGroupRedactions.builder() + .tokenGroupName("group1") + .redaction("MASK") + .build(); + + Assert.assertEquals("group1", redaction.getTokenGroupName()); + Assert.assertEquals("MASK", redaction.getRedaction()); + } + + @Test + public void testTokenGroupRedactions_defaultsAreNull() { + TokenGroupRedactions redaction = TokenGroupRedactions.builder().build(); + Assert.assertNull(redaction.getTokenGroupName()); + Assert.assertNull(redaction.getRedaction()); + } + + // BulkTokenGroupRedactions tests removed: the class was deleted; bulk detokenize now reuses + // TokenGroupRedactions (covered above). + + // ── InsertRequestRecord ───────────────────────────────────────────────────────── + + @Test + public void testInsertRequestRecord_gettersReturnBuilderValues() { + Map data = new HashMap<>(); + data.put("name", "John"); + Map tokens = new HashMap<>(); + tokens.put("name", "token-value"); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Arrays.asList("id")) + .updateType("UPDATE") + .build(); + + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("persons") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(upsert, record.getUpsert()); + } + + @Test + public void testInsertRequestRecord_defaultsAreNull() { + InsertRequestRecord record = InsertRequestRecord.builder().build(); + Assert.assertNull(record.getTableName()); + Assert.assertNull(record.getData()); + Assert.assertNull(record.getTokens()); + Assert.assertNull(record.getUpsert()); + } + + // ── BulkInsertRequestRecord ────────────────────────────────────────────── + + @Test + public void testBulkInsertRequestRecord_gettersReturnBuilderValues() { + Map data = new HashMap<>(); + data.put("name", "Jane"); + Map tokens = new HashMap<>(); + tokens.put("name", "token-1"); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Arrays.asList("id")) + .build(); + + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder() + .tableName("persons") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(upsert, record.getUpsert()); + Assert.assertEquals("REPLACE", record.getUpsert().getUpdateType()); + Assert.assertEquals(Arrays.asList("id"), record.getUpsert().getUniqueColumns()); + } + + @Test + public void testBulkInsertRequestRecord_defaultsAreNull() { + BulkInsertRequestRecord record = BulkInsertRequestRecord.builder().build(); + Assert.assertNull(record.getTableName()); + Assert.assertNull(record.getData()); + Assert.assertNull(record.getTokens()); + Assert.assertNull(record.getUpsert()); + } + + // ── TokenizeRequestRecord ─────────────────────────────────────────────────────── + + @Test + 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()); + } + + @Test + public void testTokenizeRecord_defaultsAreNull() { + TokenizeRequestRecord record = TokenizeRequestRecord.builder().build(); + Assert.assertNull(record.getValue()); + Assert.assertNull(record.getTokenGroupNames()); + } + + // ── BulkTokenizeRequestRecord ─────────────────────────────────────────────────── + + @Test + public void testBulkTokenizeRequestRecord_gettersReturnBuilderValues() { + List groups = Arrays.asList("group3"); + 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 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/RequestContextTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestContextTests.java new file mode 100644 index 00000000..6d93125d --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestContextTests.java @@ -0,0 +1,119 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; + +import java.util.Map; +import org.junit.Assert; +import org.junit.Test; + +/** + * The interceptor context: its operation, its custom headers, and the batch position. + * + *

Batch position matters without it every batch of a bulk call presents an + * identical context, so a caller cannot tag them apart — no per-batch correlation id, no + * "batch 3 of 12" logging. + */ +public class RequestContextTests { + + @Test + public void testBatchedConstructor_reportsThePosition() { + RequestContext context = new RequestContext("INSERT", 2, 5); + + Assert.assertEquals("INSERT", context.getOperation()); + Assert.assertEquals(2, context.getBatchIndex()); + Assert.assertEquals(5, context.getTotalBatches()); + } + + @Test + public void testSingleArgConstructor_reportsNotBatched() { + // Kept for source compatibility; -1 distinguishes "not batched" from "the first batch". + RequestContext context = new RequestContext("INSERT"); + + Assert.assertEquals("INSERT", context.getOperation()); + Assert.assertEquals(-1, context.getBatchIndex()); + Assert.assertEquals(-1, context.getTotalBatches()); + } + + @Test + public void testFirstBatchIsZeroNotMinusOne() { + Assert.assertEquals(0, new RequestContext("INSERT", 0, 1).getBatchIndex()); + } + + @Test + public void testHeadersStillWorkAlongsideTheBatchPosition() { + RequestContext context = new RequestContext("DETOKENIZE", 1, 3); + context.addHeader(CustomHeaderKey.REQUEST_ID_HEADER, "req-" + context.getBatchIndex()); + + Assert.assertEquals("req-1", context.getHeaders().get(CustomHeaderKey.REQUEST_ID_HEADER)); + } + + @Test + public void testHeadersRemainUnmodifiable() { + RequestContext context = new RequestContext("INSERT", 0, 1); + try { + context.getHeaders().put(CustomHeaderKey.REQUEST_ID_HEADER, "x"); + Assert.fail("the exposed header map must not be mutable"); + } catch (UnsupportedOperationException expected) { + Assert.assertTrue(true); + } + } + + // ── operation and headers (moved here with the class, from common) ────────── + @Test + public void testGetOperationReturnsConstructorValue() { + RequestContext context = new RequestContext("INSERT"); + + Assert.assertEquals("INSERT", context.getOperation()); + } + + @Test + public void testNullOperation() { + RequestContext context = new RequestContext(null); + + Assert.assertNull(context.getOperation()); + } + + @Test + public void testGetHeadersReturnsEmptyMapByDefault() { + RequestContext context = new RequestContext("INSERT"); + + Assert.assertTrue(context.getHeaders().isEmpty()); + } + + @Test + public void testAddHeaderIsReflectedInGetHeaders() { + RequestContext context = new RequestContext("INSERT"); + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "account-id-value"); + + Map headers = context.getHeaders(); + + Assert.assertEquals(1, headers.size()); + Assert.assertEquals("account-id-value", headers.get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID)); + } + + @Test + public void testAddHeaderOverwritesExistingValueForSameKey() { + RequestContext context = new RequestContext("INSERT"); + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "first-value"); + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "second-value"); + + Assert.assertEquals(1, context.getHeaders().size()); + Assert.assertEquals("second-value", context.getHeaders().get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID)); + } + + @Test + public void testAddMultipleDistinctHeaders() { + RequestContext context = new RequestContext("DETOKENIZE"); + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "account-id-value"); + context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_NAME, "account-name-value"); + + Assert.assertEquals(2, context.getHeaders().size()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testGetHeadersReturnsUnmodifiableMap() { + RequestContext context = new RequestContext("INSERT"); + + context.getHeaders().put(CustomHeaderKey.REQUEST_ID_HEADER, "request-id-value"); + } +} diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java new file mode 100644 index 00000000..0fc7bf56 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestInterceptorTests.java @@ -0,0 +1,28 @@ +package com.skyflow.vault.data; + +import com.skyflow.enums.CustomHeaderKey; +import org.junit.Assert; +import org.junit.Test; + +public class RequestInterceptorTests { + + @Test + public void testInterceptMutatesRequestContext() { + RequestInterceptor interceptor = context -> context.addHeader(CustomHeaderKey.SKYFLOW_ACCOUNT_ID, "account-id-value"); + RequestContext context = new RequestContext("INSERT"); + + interceptor.intercept(context); + + Assert.assertEquals("account-id-value", context.getHeaders().get(CustomHeaderKey.SKYFLOW_ACCOUNT_ID)); + } + + @Test + public void testInterceptorIsFunctionalInterfaceUsableAsLambda() { + final boolean[] invoked = {false}; + RequestInterceptor interceptor = context -> invoked[0] = true; + + interceptor.intercept(new RequestContext("DETOKENIZE")); + + Assert.assertTrue(invoked[0]); + } +} diff --git a/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java new file mode 100644 index 00000000..393a98d1 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/RequestResponseWrapperTests.java @@ -0,0 +1,300 @@ +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.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests for the simple request/response wrapper classes: {@link InsertRequest}, + * {@link InsertResponseRecord}, {@link DetokenizeRequest}, {@link DetokenizeResponseRecord}, + * {@link BulkDeleteTokensRequest}, {@link BulkDetokenizeRequest}, {@link BulkTokenizeRequest} + * and {@link BulkInsertRequest}. + */ +public class RequestResponseWrapperTests { + + // Tests for DeleteTokensRequest were removed: the class no longer exists (bulk-only module). + + // ── InsertRequest ──────────────────────────────────────────────────────── + + @Test + public void testInsertRequest_gettersReturnBuilderValues() { + ArrayList records = new ArrayList<>(Collections.singletonList( + InsertRequestRecord.builder().tableName("persons").build())); + UpsertOptions upsert = UpsertOptions.builder() + .uniqueColumns(Arrays.asList("id")) + .updateType("UPDATE") + .build(); + + InsertRequest request = InsertRequest.builder() + .upsert(upsert) + .records(records) + .build(); + + Assert.assertEquals(upsert, request.getUpsert()); + Assert.assertEquals(records, request.getRecords()); + } + + @Test + public void testInsertRequest_tableNameGetterReturnsBuilderValue() { + InsertRequest request = InsertRequest.builder().tableName("persons").build(); + Assert.assertEquals("persons", request.getTableName()); + } + + @Test + public void testInsertRequest_defaultsAreNull() { + InsertRequest request = InsertRequest.builder().build(); + Assert.assertNull(request.getUpsert()); + Assert.assertNull(request.getRecords()); + Assert.assertNull(request.getTableName()); + } + + // ── InsertRequestRecord ───────────────────────────────────────────────────────── + + @Test + public void testInsertRequestRecord_gettersReturnBuilderValues() { + Map data = new HashMap<>(); + data.put("name", "john"); + Map tokens = new HashMap<>(); + tokens.put("name", "tok-abc"); + UpsertOptions upsert = UpsertOptions.builder().uniqueColumns(Arrays.asList("id")).build(); + + InsertRequestRecord record = InsertRequestRecord.builder() + .tableName("persons") + .data(data) + .tokens(tokens) + .upsert(upsert) + .build(); + + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals(data, record.getData()); + Assert.assertEquals(tokens, record.getTokens()); + Assert.assertEquals(upsert, record.getUpsert()); + } + + // ── UpsertOptions ──────────────────────────────────────────────────────── + + @Test + 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()); + } + + @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()); + } + + @Test + public void testTokenizeRequest_defaultIsNull() { + TokenizeRequest request = TokenizeRequest.builder().build(); + Assert.assertNull(request.getRecords()); + } + + // ── TokenizeResponse ───────────────────────────────────────────────────── + + @Test + public void testTokenizeResponse_gettersReturnConstructorValues() { + List records = Collections.singletonList( + new TokenizeResponseRecord("value1", Collections.singletonList( + new TokenizeResponseToken("group1", "tok-abc", 200, null)))); + + TokenizeResponse response = new TokenizeResponse(records); + + 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 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")); + } + + // ── DetokenizeRequest ──────────────────────────────────────────────────── + + @Test + public void testDetokenizeRequest_gettersReturnBuilderValues() { + List tokens = Collections.singletonList("tok-1"); + List redactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASK").build()); + + DetokenizeRequest request = DetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(redactions) + .build(); + + Assert.assertEquals(tokens, request.getTokens()); + Assert.assertEquals(redactions, request.getTokenGroupRedactions()); + } + + @Test + public void testDetokenizeRequest_defaultsAreNull() { + DetokenizeRequest request = DetokenizeRequest.builder().build(); + Assert.assertNull(request.getTokens()); + Assert.assertNull(request.getTokenGroupRedactions()); + } + + // Tests for DetokenizeResponse were removed: the class no longer exists (bulk-only module). + + // ── DetokenizeResponseRecord ───────────────────────────────────────────── + + @Test + public void testDetokenizeResponseRecord_gettersReturnConstructorValues() { + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + DetokenizeResponseRecord response = new DetokenizeResponseRecord( + "tok-1", "secret-value", "group1", metadata, 200, null); + + Assert.assertEquals("tok-1", response.getToken()); + Assert.assertNull(response.getError()); + Assert.assertEquals("group1", response.getTokenGroupName()); + Assert.assertEquals(metadata, response.getMetadata()); + Assert.assertEquals(200, response.getHttpCode()); + } + + @Test + public void testDetokenizeResponseRecord_errorCase() { + DetokenizeResponseRecord response = new DetokenizeResponseRecord( + "tok-2", null, null, null, 404, "Token not found"); + + Assert.assertEquals("tok-2", response.getToken()); + Assert.assertEquals("Token not found", response.getError()); + Assert.assertNull(response.getTokenGroupName()); + Assert.assertNull(response.getMetadata()); + Assert.assertEquals(404, response.getHttpCode()); + } + + // ── BulkDeleteTokensRequest ────────────────────────────────────────────── + + @Test + public void testBulkDeleteTokensRequest_getterReturnsBuilderValue() { + List tokens = Arrays.asList("tok-1", "tok-2"); + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().tokens(tokens).build(); + Assert.assertEquals(tokens, request.getTokens()); + } + + @Test + public void testBulkDeleteTokensRequest_defaultIsNull() { + BulkDeleteTokensRequest request = BulkDeleteTokensRequest.builder().build(); + Assert.assertNull(request.getTokens()); + } + + // ── BulkDetokenizeRequest ──────────────────────────────────────────────── + + @Test + public void testBulkDetokenizeRequest_gettersReturnBuilderValues() { + List tokens = Arrays.asList("tok-1", "tok-2"); + List redactions = Collections.singletonList( + TokenGroupRedactions.builder().tokenGroupName("group1").redaction("MASK").build()); + + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(tokens) + .tokenGroupRedactions(redactions) + .build(); + + Assert.assertEquals(tokens, request.getTokens()); + Assert.assertEquals(redactions, request.getTokenGroupRedactions()); + } + + @Test + public void testBulkDetokenizeRequest_defaultsAreNull() { + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder().build(); + Assert.assertNull(request.getTokens()); + Assert.assertNull(request.getTokenGroupRedactions()); + } + + @Test + public void testBulkDetokenizeRequest_isADetokenizeRequest() { + // Bulk detokenize now shares the unary request contract; all state is inherited. + BulkDetokenizeRequest request = BulkDetokenizeRequest.builder() + .tokens(Collections.singletonList("tok-1")) + .build(); + DetokenizeRequest asUnary = request; + Assert.assertEquals(Collections.singletonList("tok-1"), asUnary.getTokens()); + } + + // ── BulkTokenizeRequest ────────────────────────────────────────────────── + + @Test + public void testBulkTokenizeRequest_getterReturnsBuilderValue() { + 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.getRecords()); + } + + // ── BulkInsertRequest ──────────────────────────────────────────────────── + + @Test + public void testBulkInsertRequest_gettersReturnBuilderValues() { + List records = new ArrayList<>(Collections.singletonList( + BulkInsertRequestRecord.builder().tableName("persons").build())); + UpsertOptions upsert = UpsertOptions.builder() + .updateType("REPLACE") + .uniqueColumns(Arrays.asList("id")) + .build(); + + BulkInsertRequest request = BulkInsertRequest.builder() + .tableName("persons") + .upsert(upsert) + .records(records) + .build(); + + Assert.assertEquals("persons", request.getTableName()); + Assert.assertEquals(upsert, request.getUpsert()); + Assert.assertEquals("REPLACE", request.getUpsert().getUpdateType()); + Assert.assertEquals(Arrays.asList("id"), request.getUpsert().getUniqueColumns()); + Assert.assertEquals(records, request.getRecords()); + } + + @Test + public void testBulkInsertRequest_defaultsAreNull() { + BulkInsertRequest request = BulkInsertRequest.builder().build(); + Assert.assertNull(request.getTableName()); + Assert.assertNull(request.getUpsert()); + Assert.assertNull(request.getRecords()); + } + + @Test + 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 new file mode 100644 index 00000000..273c4c85 --- /dev/null +++ b/flowvault/src/test/java/com/skyflow/vault/data/ResponseComponentTests.java @@ -0,0 +1,332 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +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 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 { + + // Tests for Success, Summary and Token were removed: the bulk insert response contract + // replaced those classes with BulkInsertResponseRecord / BulkSummary, covered below. + + // ── BulkInsertResponseRecord ───────────────────────────────────────────── + + @Test + public void testBulkInsertResponseRecord_gettersReturnConstructorValues() { + Map fields = new HashMap<>(); + fields.put("name", "tok-1"); + Map hashedData = new HashMap<>(); + hashedData.put("name", "hashed-1"); + + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 2, "persons", "skyflow-id-1", fields, hashedData, 200, null, null); + + Assert.assertEquals(2, record.getIndex()); + Assert.assertEquals("persons", record.getTableName()); + Assert.assertEquals("skyflow-id-1", record.getSkyflowId()); + Assert.assertEquals(fields, record.getFields()); + Assert.assertEquals(hashedData, record.getHashedData()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); + } + + @Test + public void testBulkInsertResponseRecord_errorCase() { + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 3, null, null, null, null, 500, "Internal Server Error", null); + + Assert.assertEquals(3, record.getIndex()); + Assert.assertEquals(500, record.getHttpCode()); + Assert.assertEquals("Internal Server Error", record.getError()); + Assert.assertNull(record.getTableName()); + Assert.assertNull(record.getSkyflowId()); + Assert.assertNull(record.getFields()); + Assert.assertNull(record.getHashedData()); + } + + @Test + public void testBulkInsertResponseRecord_toStringSerializesNulls() { + BulkInsertResponseRecord record = new BulkInsertResponseRecord( + 0, "persons", "skyflow-id-2", null, null, 200, null, null); + String json = record.toString(); + Assert.assertNotNull(json); + Assert.assertTrue(json.contains("skyflow-id-2")); + Assert.assertTrue(json.contains("\"index\":0")); + Assert.assertTrue(json.contains("\"fields\":null")); + } + + // ── BulkSummary ────────────────────────────────────────────────────────── + + @Test + public void testBulkSummary_noArgConstructorDefaultsToZero() { + BulkSummary summary = new BulkSummary(); + Assert.assertEquals(0, summary.getTotalRecords()); + Assert.assertEquals(0, summary.getTotalInserted()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testBulkSummary_allArgConstructor() { + BulkSummary summary = new BulkSummary(10, 8, 2); + Assert.assertEquals(10, summary.getTotalRecords()); + Assert.assertEquals(8, summary.getTotalInserted()); + Assert.assertEquals(2, summary.getTotalFailed()); + } + + @Test + public void testBulkSummary_toStringNotNull() { + Assert.assertNotNull(new BulkSummary(1, 1, 0).toString()); + } + + + // ── TokenizeResponseToken ──────────────────────────────────────────────── + + @Test + 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 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 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 ────────────────────────────────────────────────────── + + @Test + public void testTokenizeSummary_noArgConstructorDefaultsToZero() { + TokenizeSummary summary = new TokenizeSummary(); + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalTokenized()); + Assert.assertEquals(0, summary.getTotalPartial()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_allArgConstructor() { + TokenizeSummary summary = new TokenizeSummary(10, 5, 3, 2); + Assert.assertEquals(10, summary.getTotalTokens()); + Assert.assertEquals(5, summary.getTotalTokenized()); + Assert.assertEquals(3, summary.getTotalPartial()); + Assert.assertEquals(2, summary.getTotalFailed()); + } + + @Test + public void testTokenizeSummary_toStringNotNull() { + Assert.assertNotNull(new TokenizeSummary(1, 1, 0, 0).toString()); + } + + // ── 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 testBulkDeleteTokensResponseRecord_isADeleteTokensRecord() { + Assert.assertTrue(new BulkDeleteTokensResponseRecord(0, "tok-1", 200, null) instanceof DeleteTokensRecord); + } + + @Test + public void testBulkDeleteTokensResponseRecord_toStringNotNull() { + Assert.assertNotNull(new BulkDeleteTokensResponseRecord(0, "tok-1", 200, null).toString()); + } + + // ── DeleteTokensSummary ────────────────────────────────────────────────── + + @Test + public void testDeleteTokensSummary_noArgConstructorDefaultsToZero() { + DeleteTokensSummary summary = new DeleteTokensSummary(); + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalDeleted()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testDeleteTokensSummary_allArgConstructor() { + DeleteTokensSummary summary = new DeleteTokensSummary(5, 4, 1); + Assert.assertEquals(5, summary.getTotalTokens()); + Assert.assertEquals(4, summary.getTotalDeleted()); + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + public void testDeleteTokensSummary_toStringNotNull() { + Assert.assertNotNull(new DeleteTokensSummary(1, 1, 0).toString()); + } + + // ── DetokenizeSummary ──────────────────────────────────────────────────── + + @Test + public void testDetokenizeSummary_noArgConstructorDefaultsToZero() { + DetokenizeSummary summary = new DetokenizeSummary(); + Assert.assertEquals(0, summary.getTotalTokens()); + Assert.assertEquals(0, summary.getTotalDetokenized()); + Assert.assertEquals(0, summary.getTotalFailed()); + } + + @Test + public void testDetokenizeSummary_allArgConstructor() { + DetokenizeSummary summary = new DetokenizeSummary(6, 5, 1); + Assert.assertEquals(6, summary.getTotalTokens()); + Assert.assertEquals(5, summary.getTotalDetokenized()); + Assert.assertEquals(1, summary.getTotalFailed()); + } + + @Test + public void testDetokenizeSummary_toStringNotNull() { + Assert.assertNotNull(new DetokenizeSummary(1, 1, 0).toString()); + } + + // ── ErrorRecord ────────────────────────────────────────────────────────── + + @Test + public void testErrorRecord_threeArgConstructorLeavesRequestIdNull() { + ErrorRecord error = new ErrorRecord(0, "Not Found", 404); + Assert.assertEquals(0, error.getIndex()); + Assert.assertEquals("Not Found", error.getError()); + Assert.assertEquals(404, error.getCode()); + Assert.assertNull(error.getRequestId()); + } + + @Test + public void testErrorRecord_fourArgConstructorSetsRequestId() { + ErrorRecord error = new ErrorRecord(1, "Server Error", 500, "req-123"); + Assert.assertEquals(1, error.getIndex()); + Assert.assertEquals("Server Error", error.getError()); + Assert.assertEquals(500, error.getCode()); + Assert.assertEquals("req-123", error.getRequestId()); + } + + @Test + public void testErrorRecord_toStringNotNull() { + Assert.assertNotNull(new ErrorRecord(0, "err", 400).toString()); + } + + // DetokenizeResponseObject tests removed: the class was deleted; the bulk detokenize response + // contract replaced it with BulkDetokenizeResponseRecord, covered below. + + // ── BulkDetokenizeResponseRecord ───────────────────────────────────────── + + @Test + public void testBulkDetokenizeResponseRecord_gettersReturnConstructorValues() { + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 4, "tok-1", "secret-value", "group1", metadata, 200, null, null); + + Assert.assertEquals(4, record.getIndex()); + Assert.assertEquals("tok-1", record.getToken()); + Assert.assertEquals("group1", record.getTokenGroupName()); + Assert.assertEquals(metadata, record.getMetadata()); + Assert.assertEquals(200, record.getHttpCode()); + Assert.assertNull(record.getError()); + } + + @Test + public void testBulkDetokenizeResponseRecord_errorCase() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 0, "tok-2", null, null, null, 404, "Token not found", null); + + Assert.assertEquals(0, record.getIndex()); + Assert.assertEquals("Token not found", record.getError()); + Assert.assertEquals(404, record.getHttpCode()); + Assert.assertNull(record.getTokenGroupName()); + Assert.assertNull(record.getMetadata()); + } + + @Test + public void testBulkDetokenizeResponseRecord_isADetokenizeResponseRecord() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 1, "tok", "plain", "group", null, 200, null, null); + Assert.assertTrue(record instanceof DetokenizeResponseRecord); + } + + @Test + public void testBulkDetokenizeResponseRecord_toStringSerializesNulls() { + BulkDetokenizeResponseRecord record = new BulkDetokenizeResponseRecord( + 2, "tok", null, null, null, 200, null, null); + String json = record.toString(); + + Assert.assertNotNull(json); + Assert.assertTrue(json.contains("\"index\":2")); + Assert.assertTrue(json.contains("\"token\":\"tok\"")); + Assert.assertTrue(json.contains("\"error\":null")); + } +} diff --git a/pom.xml b/pom.xml index 205b5067..50d2382e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,9 @@ 4.0.0 com.skyflow - skyflow-java - 2.1.1 - jar + skyflow + 1.0.0 + pom ${project.groupId}:${project.artifactId} Skyflow SDK for the Java programming language @@ -25,6 +25,11 @@ skyflow + + common + skyvault + flowvault + scm:git:git://github.com:skyflowapi/skyflow-java.git scm:git:ssh://github.com:skyflowapi/skyflow-java.git @@ -32,20 +37,22 @@ + true 8 8 4.12.0 2.10.1 UTF-8 4.13.2 - ${project.version} + 2.3.1 + 1.3.5 com.fasterxml.jackson.core jackson-databind - 2.18.6 + 2.17.2 compile @@ -64,7 +71,7 @@ io.github.cdimascio dotenv-java - 3.2.0 + 2.2.0 com.google.code.gson @@ -89,7 +96,6 @@ 4.13.2 test - org.powermock powermock-module-junit4 @@ -105,12 +111,6 @@ - - - src/main/resources - true - - org.apache.maven.plugins @@ -131,7 +131,8 @@ 3.2.0 - com.skyflow.generated.rest.* + com.skyflow.generated.rest.*: + com.skyflow.generated.auth.*: @@ -140,6 +141,44 @@ jar + + + + + + + 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 + + + @@ -163,28 +202,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.1 - - checkstyle.xml - - false - true - - **/generated/** - - - - validate - validate - - check - - - - org.jacoco jacoco-maven-plugin @@ -255,50 +272,4 @@ https://repo.maven.apache.org/maven2/ - - - - maven-central - - - central - https://central.sonatype.com/api/v1/publisher/upload - - - central-snapshots - https://central.sonatype.com/api/v1/publisher/upload - - - - - - org.sonatype.central - central-publishing-maven-plugin - 0.4.0 - true - - central - true - true - - - - - - - jfrog - - - central - prekarilabs.jfrog.io-releases - https://prekarilabs.jfrog.io/artifactory/skyflow-java - - - snapshots - prekarilabs.jfrog.io-snapshots - https://prekarilabs.jfrog.io/artifactory/skyflow-java - - - - - + \ No newline at end of file diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index d729ea63..ccb538ba 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -1,7 +1,8 @@ # Input Arguments Version=$1 CommitHash=$2 -PomFile="$GITHUB_WORKSPACE/pom.xml" +Module=$3 +PomFile="$GITHUB_WORKSPACE/$Module/pom.xml" if [ -z "$Version" ]; then echo "Error: Version argument is required." @@ -14,27 +15,29 @@ if [ -z "$CommitHash" ]; then awk -v version="$Version" ' BEGIN { updated = 0 } + //,/<\/parent>/ { print; next } // && updated == 0 { sub(/.*<\/version>/, "" version "") updated = 1 } { print } - ' "$PomFile" > tempfile && cat tempfile > "$PomFile" && rm -f tempfile + ' "$PomFile" > tempfile && cat tempfile > "$PomFile" && rm -f tempfile echo "--------------------------" echo "Done. Main project version now at $Version" else echo "Bumping main project version to $Version-dev-$CommitHash" - awk -v version="$Version-dev.$CommitHash" ' - BEGIN { updated = 0 } - // && updated == 0 { - sub(/.*<\/version>/, "" version "") - updated = 1 - } - { print } - ' "$PomFile" > tempfile && cat tempfile > "$PomFile" && rm -f tempfile + awk -v version="$Version${CommitHash:+-dev.$CommitHash}" ' + BEGIN { updated = 0 } + //,/<\/parent>/ { print; next } + // && updated == 0 { + sub(/.*<\/version>/, "" version "") + updated = 1 + } + { print } + ' "$PomFile" > tempfile && cat tempfile > "$PomFile" && rm -f tempfile - echo "--------------------------" - echo "Done. Main project version now at $Version-dev.$CommitHash" +echo "--------------------------" +echo "Done. $Module module version now at $Version${CommitHash:+-dev.$CommitHash}" fi diff --git a/scripts/contract-snapshot-update.sh b/scripts/contract-snapshot-update.sh new file mode 100755 index 00000000..9c4c5314 --- /dev/null +++ b/scripts/contract-snapshot-update.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Regenerates a module's contract-testing baseline (/api-report/*.baseline.jar) +# from the CURRENT working tree and overwrites the committed snapshot. +# +# Run this after an intentional public API change, review the resulting git diff on +# the jar (a new binary blob) alongside your code change, and commit both together. +# This is the only way a committed baseline should ever change - japicmp never pulls +# a published version for the comparison. +# +# scripts/contract-snapshot-update.sh skyvault # regenerate one module +# scripts/contract-snapshot-update.sh flowvault +# scripts/contract-snapshot-update.sh # both +# +# Prefer naming the module you actually changed. Jar archives embed timestamps, so +# regenerating a module whose API did not change still produces different bytes and +# a spurious diff on a binary file - which is exactly the thing a reviewer cannot +# eyeball. Only the baseline you intend to move should appear in the commit. +set -euo pipefail + +cd "$(dirname "$0")/.." + +# module -> artifactId, which is also the baseline jar's name +declare -A ARTIFACTS=( + [skyvault]="skyflow-java" + [flowvault]="skyflow-flowvault-java" +) + +MODULES=("$@") +if [ ${#MODULES[@]} -eq 0 ]; then + MODULES=(skyvault flowvault) +fi + +for MODULE in "${MODULES[@]}"; do + ARTIFACT="${ARTIFACTS[$MODULE]:-}" + if [ -z "$ARTIFACT" ]; then + echo "Error: unknown module '$MODULE'. Expected one of: ${!ARTIFACTS[*]}" + exit 1 + fi + + echo "=== $MODULE ===" + mvn -B package -pl "common,$MODULE" -am -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true + + # the comparison-only jar, which merges com.skyflow:common into the module + SHADED_JAR=$(ls "$MODULE"/target/"$ARTIFACT"-*-with-common.jar 2>/dev/null | head -n1) + if [ -z "$SHADED_JAR" ]; then + echo "Error: could not find $MODULE/target/$ARTIFACT-*-with-common.jar. Did the build succeed?" + exit 1 + fi + + mkdir -p "$MODULE/api-report" + cp "$SHADED_JAR" "$MODULE/api-report/$ARTIFACT.baseline.jar" + echo "Updated $MODULE/api-report/$ARTIFACT.baseline.jar from $SHADED_JAR" +done + +echo "--------------------------" +echo "Review the diff and commit the baseline jar(s) alongside your API change." diff --git a/scripts/current_module_version.sh b/scripts/current_module_version.sh new file mode 100755 index 00000000..0bab3458 --- /dev/null +++ b/scripts/current_module_version.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Prints 's own current (skipping the inherited +# block, which has its own tag), with any existing -dev. +# suffix stripped. Read-only - never modifies the pom. +# +# Used by internal releases to get a module's base version without touching +# git tags at all, so it can never accidentally pick up another module's tag. +set -euo pipefail + +Module=$1 +PomFile="$Module/pom.xml" + +raw_line=$(awk ' + //,/<\/parent>/ { next } + // { print; exit } +' "$PomFile") + +version=$(echo "$raw_line" | sed -E 's#.*([^<]+).*#\1#') +version=$(echo "$version" | sed -E 's/-dev\.[0-9a-f]+$//') + +echo "$version" diff --git a/skyvault/README.md b/skyvault/README.md new file mode 100644 index 00000000..e996553a --- /dev/null +++ b/skyvault/README.md @@ -0,0 +1,3151 @@ +# Skyflow Java + +> **This SDK brings flexible auth, multi-vault support, builder patterns, native data types, and rich error diagnostics.** +> +> Meant for **Privacy DB** vaults. +> +> Migrating from v1? See the **[Migration Guide](../docs/migrate_to_v2.md)** for step-by-step instructions. V1 is in maintenance mode and will reach End of Life on October 31, 2026. + +The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend. + +[![CI](https://img.shields.io/static/v1?label=CI&message=passing&color=green?style=plastic&logo=github)](https://github.com/skyflowapi/skyflow-java/actions) +[![GitHub release](https://img.shields.io/github/v/release/skyflowapi/skyflow-java.svg)](https://mvnrepository.com/artifact/com.skyflow/skyflow-java) +[![License](https://img.shields.io/github/license/skyflowapi/skyflow-java)](https://github.com/skyflowapi/skyflow-java/blob/main/LICENSE) + +# Table of Contents + +- [Table of Contents](#table-of-contents) +- [Overview](#overview) +- [Install](#install) + - [Requirements](#requirements) + - [Configuration](#configuration) + - [Gradle users](#gradle-users) + - [Maven users](#maven-users) +- [API Reference](../docs/api_reference.md) +- [Migration from v1 to v2](../docs/migrate_to_v2.md) +- [Quickstart](#quickstart) + - [Authenticate](#authenticate) + - [Initialize the client](#initialize-the-client) + - [Insert data into the vault](#insert-data-into-the-vault) +- [Vault](#vault) + - [VaultController](#vaultcontroller) + - [Insert data into the vault](#insert-data-into-the-vault-1) + - [Detokenize](#detokenize) + - [DetokenizeRecordResponse](#detokenizerecordresponse) + - [Tokenize](#tokenize) + - [Get](#get) + - [Get by skyflow IDS](#get-by-skyflow-ids) + - [Get tokens](#get-tokens) + - [Get by column name and column values](#get-by-column-name-and-column-values) + - [Redaction types](#redaction-types) + - [Update](#update) + - [Delete](#delete) + - [Query](#query) + - [Upload File](#upload-file) + +- [Detect](#detect) + - [Deidentify Text](#deidentify-text) + - [Reidentify Text](#reidentify-text) + - [Deidentify File](#deidentify-file) + - [Get Run](#get-run) + - [Detect response types](#detect-response-types) + - [Detect enums](#detect-enums) +- [Connections](#connections) + - [ConnectionController](#connectioncontroller) + - [Invoke a connection](#invoke-a-connection) +- [Client Management](#client-management) +- [Authenticate with bearer tokens](#authenticate-with-bearer-tokens) + - [Generate a bearer token](#generate-a-bearer-token) + - [Generate bearer tokens with context](#generate-bearer-tokens-with-context) + - [Generate scoped bearer tokens](#generate-scoped-bearer-tokens) + - [Generate signed data tokens](#generate-signed-data-tokens) + - [Bearer token expiry edge case](#bearer-token-expiry-edge-case) +- [Error Handling](#error-handling) + - [Catching SkyflowException](#catching-skyflowexception) + - [SkyflowException properties](#skyflowexception-properties) +- [Logging](#logging) +- [Reporting a Vulnerability](#reporting-a-vulnerability) + +# Overview + +- Authenticate using a Skyflow service account and generate bearer tokens for secure access. +- Perform Vault API operations such as inserting, retrieving, and tokenizing sensitive data with ease. +- Invoke connections to third-party APIs without directly handling sensitive data, ensuring compliance and data protection. + +> [!TIP] +> Looking for the full list of request builder methods, response getters, enums, helper class APIs, and service-account utilities? See the **[API Reference](../docs/api_reference.md)**. + +# Install + +## Requirements + +- Java 8 and above (tested with Java 8) + +## Configuration + +--- + +### Gradle users + +Add this dependency to your project's `build.gradle` file: + +``` +implementation 'com.skyflow:skyflow-java:2.0.0' +``` + +### Maven users + +Add this dependency to your project's `pom.xml` file: + +```xml + + com.skyflow + skyflow-java + 2.0.0 + +``` + +--- + +# Migrate from v1 to v2 + +Upgrading from v1? See the dedicated migration guide: **[../docs/migrate_to_v2.md](../docs/migrate_to_v2.md)** + +# Quickstart + +Get started quickly with the essential steps: authenticate, initialize the client, and perform a basic vault operation. This section provides a minimal setup to help you integrate the SDK efficiently. + +### Authenticate + +You can use an API key to authenticate and authorize requests to an API. For authenticating via bearer tokens and different supported bearer token types, refer to the [Authenticate with bearer tokens](#authenticate-with-bearer-tokens) section. + +```java +// create a new credentials object +Credentials credentials = new Credentials(); +credentials.setApiKey(""); // add your API key in credentials +``` + +### Initialize the client + +To get started, you must first initialize the skyflow client. While initializing the skyflow client, you can specify different types of credentials. + +1. **API keys** + A unique identifier used to authenticate and authorize requests to an API. + +2. **Bearer tokens** + A temporary access token used to authenticate API requests, typically included in the Authorization header. + +3. **Service account credentials file path** + The file path pointing to a JSON file containing credentials for a service account, used for secure API access. + +4. **Service account credentials string (JSON formatted)** + A JSON-formatted string containing service account credentials, often used as an alternative to a file for programmatic authentication. + +Note: Only one type of credential can be used at a time. If multiple credentials are provided, the last one added will take precedence. + +```java +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; + +/** + * Example program to initialize the Skyflow client with various configurations. + * The Skyflow client facilitates secure interactions with the Skyflow vault, + * such as securely managing sensitive data. + */ +public class InitSkyflowClient { + public static void main(String[] args) throws SkyflowException { + // Step 1: Define the primary credentials for authentication. + // Note: Only one type of credential can be used at a time. You can choose between: + // - API key + // - Bearer token + // - A credentials string (JSON-formatted) + // - A file path to a credentials file. + + // Initialize primary credentials using a Bearer token for authentication. + Credentials primaryCredentials = new Credentials(); + primaryCredentials.setToken(""); // Replace with your actual authentication token. + + // Step 2: Configure the primary vault details. + // VaultConfig stores all necessary details to connect to a specific Skyflow vault. + VaultConfig primaryConfig = new VaultConfig(); + primaryConfig.setVaultId(""); // Replace with your primary vault's ID. + primaryConfig.setClusterId(""); // Replace with the cluster ID (part of the vault URL, e.g., https://{clusterId}.vault.skyflowapis.com). + primaryConfig.setEnv(Env.PROD); // Set the environment (PROD, SANDBOX, STAGE, DEV). + primaryConfig.setCredentials(primaryCredentials); // Attach the primary credentials to this vault configuration. + + // Step 3: Create credentials as a JSON object (if a Bearer Token is not provided). + // Demonstrates an alternate approach to authenticate with Skyflow using a credentials object. + JsonObject credentialsObject = new JsonObject(); + credentialsObject.addProperty("clientId", ""); // Replace with your Client ID. + credentialsObject.addProperty("clientName", ""); // Replace with your Client Name. + credentialsObject.addProperty("tokenUri", ""); // Replace with the Token URI. + credentialsObject.addProperty("keyId", ""); // Replace with your Key ID. + credentialsObject.addProperty("privateKey", ""); // Replace with your Private Key. + + // Step 4: Convert the JSON object to a string and use it as credentials. + // This approach allows the use of dynamically generated or pre-configured credentials. + Credentials skyflowCredentials = new Credentials(); + skyflowCredentials.setCredentialsString(credentialsObject.toString()); // Converts JSON object to string for use as credentials. + + // Step 5: Define secondary credentials (API key-based authentication as an example). + // Demonstrates a different type of authentication mechanism for Skyflow vaults. + Credentials secondaryCredentials = new Credentials(); + secondaryCredentials.setApiKey(""); // Replace with your API Key for authentication. + + // Step 6: Configure the secondary vault details. + // A secondary vault configuration can be used for operations involving multiple vaults. + VaultConfig secondaryConfig = new VaultConfig(); + secondaryConfig.setVaultId(""); // Replace with your secondary vault's ID. + secondaryConfig.setClusterId(""); // Replace with the corresponding cluster ID. + secondaryConfig.setEnv(Env.SANDBOX); // Set the environment for this vault. + secondaryConfig.setCredentials(secondaryCredentials); // Attach the secondary credentials to this configuration. + + // Step 7: Define tertiary credentials using a path to a credentials JSON file. + // This method demonstrates an alternative authentication method. + Credentials tertiaryCredentials = new Credentials(); + tertiaryCredentials.setPath(""); // Replace with the path to your credentials file. + + // Step 8: Configure the tertiary vault details. + VaultConfig tertiaryConfig = new VaultConfig(); + tertiaryConfig.setVaultId(""); // Replace with the tertiary vault ID. + tertiaryConfig.setClusterId(""); // Replace with the corresponding cluster ID. + tertiaryConfig.setEnv(Env.STAGE); // Set the environment for this vault. + tertiaryConfig.setCredentials(tertiaryCredentials); // Attach the tertiary credentials. + + // Step 9: Build and initialize the Skyflow client. + // Skyflow client is configured with multiple vaults and credentials. + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.INFO) // Set log level for debugging or monitoring purposes. + .addVaultConfig(primaryConfig) // Add the primary vault configuration. + .addVaultConfig(secondaryConfig) // Add the secondary vault configuration. + .addVaultConfig(tertiaryConfig) // Add the tertiary vault configuration. + .addSkyflowCredentials(skyflowCredentials) // Add JSON-formatted credentials if applicable. + .build(); + + // The Skyflow client is now fully initialized. + // Use the `skyflowClient` object to perform secure operations such as: + // - Inserting data + // - Retrieving data + // - Deleting data + // within the configured Skyflow vaults. + } +} +``` + +Notes: + +- If both Skyflow common credentials and individual credentials at the configuration level are specified, the individual credentials at the configuration level will take precedence. +- If neither Skyflow common credentials nor individual configuration-level credentials are provided, the SDK attempts to retrieve credentials from the `SKYFLOW_CREDENTIALS` environment variable. +- All Vault operations require a client instance. +- `Credentials.setContext()` accepts either a `String` or a `Map` for context-aware authorization. See [Generate bearer tokens with context](#generate-bearer-tokens-with-context) for full usage. + +### Insert data into the vault + +To insert data into your vault, use the `insert` method. The `InsertRequest` class creates an insert request, which includes the values to be inserted as a list of records. Below is a simple example to get started. For advanced options, check out [Insert data into the vault](#insert-data-into-the-vault-1) section. + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This example demonstrates how to insert sensitive data (e.g., card information) into a Skyflow vault using the Skyflow client. + * + * 1. Initializes the Skyflow client. + * 2. Prepares a record with sensitive data (e.g., card number and cardholder name). + * 3. Creates an insert request for inserting the data into the Skyflow vault. + * 4. Prints the response of the insert operation. + */ +public class InsertExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize data to be inserted into the Skyflow vault + ArrayList> insertData = new ArrayList<>(); + + // Create a HashMap for a single record with card number and cardholder name as fields + HashMap insertRecord = new HashMap<>(); + insertRecord.put("card_number", "4111111111111111"); // Replace with actual card number (sensitive data) + insertRecord.put("cardholder_name", "john doe"); // Replace with actual cardholder name (sensitive data) + + // Add the created record to the list of data to be inserted + insertData.add(insertRecord); + + // Step 2: Build the InsertRequest object with the table name and data to insert + InsertRequest insertRequest = InsertRequest.builder() + .table("table1") // Specify the table in the vault where the data will be inserted + .values(insertData) // Attach the data (records) to be inserted + .returnTokens(true) // Specify if tokens should be returned upon successful insertion + .build(); // Build the insert request object + + // Step 3: Perform the insert operation using the Skyflow client + InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); + // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 4: Print the response from the insert operation + System.out.println(insertResponse); + } catch (SkyflowException e) { + // Step 5: Handle any exceptions that may occur during the insert operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the stack trace for debugging purposes + } + } +} +``` + +Skyflow returns tokens for the record that was just inserted. + +```json +{ + "insertedFields": [ + { + "card_number": "5484-7829-1702-9110", + "requestIndex": "0", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" + } + ], + "errors": [] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +# Vault + +The [Vault](https://github.com/skyflowapi/skyflow-java/tree/main/samples/src/main/java/com/example/vault) module performs operations on the vault, including inserting records, detokenizing tokens, and retrieving tokens associated with a `skyflow_id`. + +## VaultController + +`VaultController` is the class returned by `skyflowClient.vault()` and `skyflowClient.vault(vaultId)`. All vault operations are called on this object. + +```java +// Uses the default (first configured) vault +VaultController vault = skyflowClient.vault(); + +// Uses a specific vault by ID +VaultController vault = skyflowClient.vault(""); +``` + +**Methods:** + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `insert(InsertRequest)` | [`InsertRequest`](../docs/api_reference.md#insertrequest) | [`InsertResponse`](../docs/api_reference.md#insertresponse) | Insert one or more records | +| `detokenize(DetokenizeRequest)` | [`DetokenizeRequest`](../docs/api_reference.md#detokenizerequest) | [`DetokenizeResponse`](../docs/api_reference.md#detokenizeresponse) | Detokenize tokens to their original values | +| `tokenize(TokenizeRequest)` | [`TokenizeRequest`](../docs/api_reference.md#tokenizerequest) | [`TokenizeResponse`](../docs/api_reference.md#tokenizeresponse) | Tokenize sensitive values | +| `get(GetRequest)` | [`GetRequest`](../docs/api_reference.md#getrequest) | [`GetResponse`](../docs/api_reference.md#getresponse) | Retrieve records by Skyflow ID or column value | +| `update(UpdateRequest)` | [`UpdateRequest`](../docs/api_reference.md#updaterequest) | [`UpdateResponse`](../docs/api_reference.md#updateresponse) | Update a record by Skyflow ID | +| `delete(DeleteRequest)` | [`DeleteRequest`](../docs/api_reference.md#deleterequest) | [`DeleteResponse`](../docs/api_reference.md#deleteresponse) | Delete records by Skyflow ID | +| `query(QueryRequest)` | [`QueryRequest`](../docs/api_reference.md#queryrequest) | [`QueryResponse`](../docs/api_reference.md#queryresponse) | Execute a SQL query | +| `uploadFile(FileUploadRequest)` | [`FileUploadRequest`](../docs/api_reference.md#fileuploadrequest) | [`FileUploadResponse`](../docs/api_reference.md#fileuploadresponse) | Upload a file to a vault column | + +All methods throw `SkyflowException` on error. + +## Insert data into the vault + +Apart from using the `insert` method to insert data into your vault covered in [Quickstart](#quickstart), you can also specify options in [`InsertRequest`](../docs/api_reference.md#insertrequest), such as returning tokenized data, upserting records, or continuing the operation in case of errors. Returns an [`InsertResponse`](../docs/api_reference.md#insertresponse). + +### Construct an insert request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Example program to demonstrate inserting data into a Skyflow vault, along with corresponding InsertRequest schema. + * + */ +public class InsertSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Prepare the data to be inserted into the Skyflow vault + ArrayList> insertData = new ArrayList<>(); + + // Create the first record with field names and their respective values + HashMap insertRecord1 = new HashMap<>(); + insertRecord1.put("", ""); // Replace with actual field name and value + insertRecord1.put("", ""); // Replace with actual field name and value + + // Create the second record with field names and their respective values + HashMap insertRecord2 = new HashMap<>(); + insertRecord2.put("", ""); // Replace with actual field name and value + insertRecord2.put("", ""); // Replace with actual field name and value + + // Add the records to the list of data to be inserted + insertData.add(insertRecord1); + insertData.add(insertRecord2); + + // Step 2: Build an InsertRequest object with the table name and the data to insert + InsertRequest insertRequest = InsertRequest.builder() + .table("") // Replace with the actual table name in your Skyflow vault + .values(insertData) // Attach the data to be inserted + .build(); + + // Step 3: Use the Skyflow client to perform the insert operation + InsertResponse insertResponse = skyflowClient.vault("").insert(insertRequest); + // Replace with your actual vault ID + + // Print the response from the insert operation + System.out.println("Insert Response: " + insertResponse); + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the insert operation + System.out.println("Error occurred while inserting data: "); + e.printStackTrace(); // Print the stack trace for debugging + } + } +} +``` + +### Insert call [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/InsertExample.java) with `continueOnError` option + +The `continueOnError` flag is a boolean that determines whether insert operation should proceed despite encountering partial errors. Set to `true` to allow the process to continue even if some errors occur. + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This example demonstrates how to insert multiple records into a Skyflow vault using the Skyflow client. + * + * 1. Initializes the Skyflow client. + * 2. Prepares multiple records with sensitive data (e.g., card number and cardholder name). + * 3. Creates an insert request with the records to insert into the Skyflow vault. + * 4. Specifies options to continue on error and return tokens. + * 5. Prints the response of the insert operation. + */ +public class InsertExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list to hold the data records to be inserted into the vault + ArrayList> insertData = new ArrayList<>(); + + // Step 2: Create the first record with card number and cardholder name + HashMap insertRecord1 = new HashMap<>(); + insertRecord1.put("card_number", "4111111111111111"); // Replace with actual card number (sensitive data) + insertRecord1.put("cardholder_name", "john doe"); // Replace with actual cardholder name (sensitive data) + + // Step 3: Create the second record with card number and cardholder name + HashMap insertRecord2 = new HashMap<>(); + insertRecord2.put("card_number", "4111111111111111"); // Ensure field name matches ("card_number") + insertRecord2.put("cardholder_name", "jane doe"); // Replace with actual cardholder name (sensitive data) + + // Step 4: Add the records to the insertData list + insertData.add(insertRecord1); + insertData.add(insertRecord2); + + // Step 5: Build the InsertRequest object with the data records to insert + InsertRequest insertRequest = InsertRequest.builder() + .table("table1") // Specify the table in the vault where data will be inserted + .values(insertData) // Attach the data records to be inserted + .returnTokens(true) // Specify if tokens should be returned upon successful insertion + .continueOnError(true) // Specify to continue inserting records even if an error occurs for some records + .build(); // Build the insert request object + + // Step 6: Perform the insert operation using the Skyflow client + InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); + // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 7: Print the response from the insert operation + System.out.println(insertResponse); + } catch (SkyflowException e) { + // Step 8: Handle any exceptions that may occur during the insert operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the stack trace for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "insertedFields": [ + { + "card_number": "5484-7829-1702-9110", + "requestIndex": "0", + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" + } + ], + "errors": [ + { + "requestIndex": "1", + "error": "Insert failed. Column card_number is invalid. Specify a valid column." + } + ] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +### Insert call example with `upsert` option + +An upsert operation checks for a record based on a unique column's value. If a match exists, the record is updated; otherwise, a new record is inserted. + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.InsertResponse; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This example demonstrates how to insert or upsert a record into a Skyflow vault using the Skyflow client, with the option to return tokens. + * + * 1. Initializes the Skyflow client. + * 2. Prepares a record to insert or upsert (e.g., cardholder name). + * 3. Creates an insert request with the data to be inserted or upserted into the Skyflow vault. + * 4. Specifies the field (cardholder_name) for upsert operations. + * 5. Prints the response of the insert or upsert operation. + */ +public class UpsertExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list to hold the data records for the insert/upsert operation + ArrayList> upsertData = new ArrayList<>(); + + // Step 2: Create a record with the field 'cardholder_name' to insert or upsert + HashMap upsertRecord = new HashMap<>(); + upsertRecord.put("cardholder_name", "jane doe"); // Replace with the actual cardholder name + + // Step 3: Add the record to the upsertData list + upsertData.add(upsertRecord); + + // Step 4: Build the InsertRequest object with the upsertData + InsertRequest insertRequest = InsertRequest.builder() + .table("table1") // Specify the table in the vault where data will be inserted/upserted + .values(upsertData) // Attach the data records to be inserted/upserted + .returnTokens(true) // Specify if tokens should be returned upon successful operation + .upsert("cardholder_name") // Specify the field to be used for upsert operations (e.g., cardholder_name) + .build(); // Build the insert request object + + // Step 5: Perform the insert/upsert operation using the Skyflow client + InsertResponse insertResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").insert(insertRequest); + // Replace the vault ID "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 6: Print the response from the insert/upsert operation + System.out.println(insertResponse); + } catch (SkyflowException e) { + // Step 7: Handle any exceptions that may occur during the insert/upsert operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the stack trace for debugging purposes + } + } +} +``` + +Skyflow returns tokens, with `upsert` support, for the record you just inserted. + +```json +{ + "insertedFields": [ + { + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "cardholder_name": "73ce45ce-20fd-490e-9310-c1d4f603ee83" + } + ], + "errors": [] +} +``` + +## Detokenize + +To retrieve tokens from your vault, use the `detokenize` method. [`DetokenizeRequest`](../docs/api_reference.md#detokenizerequest) requires a list of detokenization data as input. Returns a [`DetokenizeResponse`](../docs/api_reference.md#detokenizeresponse). + +### Construct a detokenize request + +Each entry in the detokenize list is a [`DetokenizeData`](../docs/api_reference.md#detokenizedata) object pairing a token with its desired redaction type. See the [API Reference](../docs/api_reference.md#detokenizerequest) for all `DetokenizeRequest` builder options. + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to detokenize sensitive data from tokens stored in a Skyflow vault, along with corresponding DetokenizeRequest schema. + * + */ +public class DetokenizeSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of tokens to be detokenized (replace with actual tokens) + ArrayList detokenizeData1 = new ArrayList<>(); + DetokenizeData detokenizeDataRecord1 = new DetokenizeData("", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction + DetokenizeData detokenizeDataRecord2 = new DetokenizeData("", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction + detokenizeData1.add(detokenizeDataRecord1); + detokenizeData1.add(detokenizeDataRecord2); + + // Step 2: Create the DetokenizeRequest object with the tokens and redaction type + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types + .continueOnError(true) // Continue even if one token cannot be detokenized + .build(); // Build the detokenization request + + // Step 3: Call the Skyflow vault to detokenize the provided tokens + DetokenizeResponse detokenizeResponse = skyflowClient.vault("").detokenize(detokenizeRequest); + // Replace with your actual Skyflow vault ID + + // Step 4: Print the detokenization response, which contains the detokenized data + System.out.println(detokenizeResponse); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the detokenization process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Notes: + +- `redactionType` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). +- `continueOnError` defaults to `true`. + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/DetokenizeExample.java) of a detokenize call: + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to detokenize sensitive data from tokens stored in a Skyflow vault. + * + * 1. Initializes the Skyflow client. + * 2. Creates a list of tokens (e.g., credit card tokens) that represent the sensitive data. + * 3. Builds a detokenization request using the provided tokens and specifies how the redacted data should be returned. + * 4. Calls the Skyflow vault to detokenize the tokens and retrieves the detokenized data. + * 5. Prints the detokenization response, which contains the detokenized values or errors. + */ +public class DetokenizeExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) + ArrayList detokenizeData1 = new ArrayList<>(); + DetokenizeData detokenizeDataRecord1 = new DetokenizeData("9738-1683-0486-1480", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction + DetokenizeData detokenizeDataRecord2 = new DetokenizeData("6184-6357-8409-6668", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction + detokenizeData1.add(detokenizeDataRecord1); + detokenizeData1.add(detokenizeDataRecord2); + + // Step 2: Create the DetokenizeRequest object with the tokens and redaction type + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types + .continueOnError(true) // Continue even if one token cannot be detokenized + .build(); // Build the detokenization request + + // Step 3: Call the Skyflow vault to detokenize the provided tokens + DetokenizeResponse detokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").detokenize(detokenizeRequest); + // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 4: Print the detokenization response, which contains the detokenized data + System.out.println(detokenizeResponse); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the detokenization process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "detokenizedFields": [{ + "token": "9738-1683-0486-1480", + "value": "4111111111111115", + "type": "STRING", + }, { + "token": "6184-6357-8409-6668", + "value": "4111111111111119", + "type": "STRING", + }], + "errors": [] +} + +``` + +### DetokenizeRecordResponse + +`DetokenizeResponse.getDetokenizedFields()` and `DetokenizeResponse.getErrors()` each return a `List`. Use this class to read individual token results: + +```java +DetokenizeResponse detokenizeResponse = skyflowClient.vault("").detokenize(detokenizeRequest); + +for (DetokenizeRecordResponse record : detokenizeResponse.getDetokenizedFields()) { + System.out.println("Token : " + record.getToken()); + System.out.println("Value : " + record.getValue()); + System.out.println("Type : " + record.getType()); + System.out.println("ReqID : " + record.getRequestId()); +} + +for (DetokenizeRecordResponse err : detokenizeResponse.getErrors()) { + System.out.println("Failed token : " + err.getToken()); + System.out.println("Error : " + err.getError()); +} +``` + +See [`DetokenizeRecordResponse`](../docs/api_reference.md#detokenizerecordresponse) in the API Reference for the full attribute list. + +### An example of a detokenize call with `continueOnError` option: + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.DetokenizeResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to detokenize sensitive data (e.g., credit card numbers) from tokens in a Skyflow vault. + * + * 1. Initializes the Skyflow client. + * 2. Creates a list of tokens (e.g., credit card tokens) to be detokenized. + * 3. Builds a detokenization request with the tokens and specifies the redaction type for the detokenized data. + * 4. Calls the Skyflow vault to detokenize the tokens and retrieves the detokenized data. + * 5. Prints the detokenization response, which includes the detokenized values or errors. + */ +public class DetokenizeExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) + // Step 1: Initialize a list of tokens to be detokenized (replace with actual token values) + ArrayList detokenizeData1 = new ArrayList<>(); + DetokenizeData detokenizeDataRecord1 = new DetokenizeData("9738-1683-0486-1480", RedactionType.PLAIN_TEXT); // Replace with a token to detokenize with PLAIN_TEXT redaction + DetokenizeData detokenizeDataRecord2 = new DetokenizeData("6184-6357-8409-6668", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction + DetokenizeData detokenizeDataRecord2 = new DetokenizeData("4914-9088-2814-384", RedactionType.PLAIN_TEXT); // Replace with another token to detokenize with PLAIN_TEXT redaction + + detokenizeData1.add(detokenizeDataRecord1); + detokenizeData1.add(detokenizeDataRecord2); + + // Step 2: Create the DetokenizeRequest object with the tokens and redaction type + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeData1) // Specify detokenize data with specified redaction types + .continueOnError(true) // Continue even if one token cannot be detokenized + .build(); // Build the detokenization request + + // Step 3: Call the Skyflow vault to detokenize the provided tokens + DetokenizeResponse detokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").detokenize(detokenizeRequest); + // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 4: Print the detokenization response, which contains the detokenized data or errors + System.out.println(detokenizeResponse); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the detokenization process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "detokenizedFields": [{ + "token": "9738-1683-0486-1480", + "value": "4111111111111115", + "type": "STRING", + }, { + "token": "6184-6357-8409-6668", + "value": "4111111111111119", + "type": "STRING", + }], + "errors": [{ + "token": "4914-9088-2814-384", + "error": "Token Not Found", + }] +} +``` + +## Tokenize + +Tokenization replaces sensitive data with unique identifier tokens. This approach protects sensitive information by securely storing the original data while allowing the use of tokens within your application. + +To tokenize data, use the `tokenize` method. [`TokenizeRequest`](../docs/api_reference.md#tokenizerequest) accepts a list of [`ColumnValue`](../docs/api_reference.md#columnvalue) objects. Returns a [`TokenizeResponse`](../docs/api_reference.md#tokenizeresponse). + +### Construct a tokenize request + +Each entry in the tokenize list is a [`ColumnValue`](../docs/api_reference.md#columnvalue) object pairing a value with its column group. See the [API Reference](../docs/api_reference.md#tokenizerequest) for all `TokenizeRequest` builder options. + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.ColumnValue; +import com.skyflow.vault.tokens.TokenizeRequest; +import com.skyflow.vault.tokens.TokenizeResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to tokenize sensitive data (e.g., credit card information) using the Skyflow client, along with corresponding TokenizeRequest schema. + * + */ +public class TokenizeSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of column values to be tokenized (replace with actual sensitive data) + ArrayList columnValues = new ArrayList<>(); + + // Step 2: Create column values for each sensitive data field (e.g., card number and cardholder name) + ColumnValue columnValue1 = ColumnValue.builder().value("").columnGroup("").build(); // Replace and with actual data + ColumnValue columnValue2 = ColumnValue.builder().value("").columnGroup("").build(); // Replace and with actual data + + // Add the created column values to the list + columnValues.add(columnValue1); + columnValues.add(columnValue2); + + // Step 3: Build the TokenizeRequest with the column values + TokenizeRequest tokenizeRequest = TokenizeRequest.builder().values(columnValues).build(); + + // Step 4: Call the Skyflow vault to tokenize the sensitive data + TokenizeResponse tokenizeResponse = skyflowClient.vault("").tokenize(tokenizeRequest); + // Replace with your actual Skyflow vault ID + + // Step 5: Print the tokenization response, which contains the generated tokens or errors + System.out.println(tokenizeResponse); + } catch (SkyflowException e) { + // Step 6: Handle any errors that occur during the tokenization process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/TokenizeExample.java) of Tokenize call: + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.tokens.ColumnValue; +import com.skyflow.vault.tokens.TokenizeRequest; +import com.skyflow.vault.tokens.TokenizeResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to tokenize sensitive data (e.g., credit card information) using the Skyflow client. + * + * 1. Initializes the Skyflow client. + * 2. Creates a column value for sensitive data (e.g., credit card number). + * 3. Builds a tokenize request with the column value to be tokenized. + * 4. Sends the request to the Skyflow vault for tokenization. + * 5. Prints the tokenization response, which includes the token or errors. + */ +public class TokenizeExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of column values to be tokenized (replace with actual sensitive data) + ArrayList columnValues = new ArrayList<>(); + + // Step 2: Create a column value for the sensitive data (e.g., card number with its column group) + ColumnValue columnValue = ColumnValue.builder() + .value("4111111111111111") // Replace with the actual sensitive data (e.g., card number) + .columnGroup("card_number_cg") // Replace with the actual column group name + .build(); + + // Add the created column value to the list + columnValues.add(columnValue); + + // Step 3: Build the TokenizeRequest with the column value + TokenizeRequest tokenizeRequest = TokenizeRequest.builder().values(columnValues).build(); + + // Step 4: Call the Skyflow vault to tokenize the sensitive data + TokenizeResponse tokenizeResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").tokenize(tokenizeRequest); + // Replace "9f27764a10f7946fe56b3258e117" with your actual Skyflow vault ID + + // Step 5: Print the tokenization response, which contains the generated token or any errors + System.out.println(tokenizeResponse); + } catch (SkyflowException e) { + // Step 6: Handle any errors that occur during the tokenization process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "tokens": [5479-4229-4622-1393] +} +``` + +## Get + +To retrieve data using Skyflow IDs or unique column values, use the `get` method. [`GetRequest`](../docs/api_reference.md#getrequest) accepts parameters such as table name, redaction type, Skyflow IDs, column names, and column values. `ids` and `columnName`/`columnValues` are mutually exclusive. Returns a [`GetResponse`](../docs/api_reference.md#getresponse). + +### Construct a get request + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to retrieve data from the Skyflow vault using different methods, along with corresponding GetRequest schema. + * + */ +public class GetSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of Skyflow IDs to retrieve records (replace with actual Skyflow IDs) + ArrayList ids = new ArrayList<>(); + ids.add(""); // Replace with actual Skyflow ID + ids.add(""); // Replace with actual Skyflow ID + + // Step 2: Create a GetRequest to retrieve records by Skyflow ID without returning tokens + GetRequest getByIdRequest = GetRequest.builder() + .ids(ids) + .table("") // Replace with the actual table name + .returnTokens(false) // Set to false to avoid returning tokens + .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text + .build(); + + // Send the request to the Skyflow vault and retrieve the records + GetResponse getByIdResponse = skyflowClient.vault("").get(getByIdRequest); // Replace with actual Vault ID + System.out.println(getByIdResponse); + + // Step 3: Create another GetRequest to retrieve records by Skyflow ID with tokenized values + GetRequest getTokensRequest = GetRequest.builder() + .ids(ids) + .table("") // Replace with the actual table name + .returnTokens(true) // Set to true to return tokenized values + .build(); + + // Send the request to the Skyflow vault and retrieve the tokenized records + GetResponse getTokensResponse = skyflowClient.vault("").get(getTokensRequest); // Replace with actual Vault ID + System.out.println(getTokensResponse); + + // Step 4: Create a GetRequest to retrieve records based on specific column values + ArrayList columnValues = new ArrayList<>(); + columnValues.add(""); // Replace with the actual column value + columnValues.add(""); // Replace with the actual column value + + GetRequest getByColumnRequest = GetRequest.builder() + .table("") // Replace with the actual table name + .columnName("") // Replace with the column name + .columnValues(columnValues) // Add the list of column values to filter by + .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text + .build(); + + // Send the request to the Skyflow vault and retrieve the records filtered by column values + GetResponse getByColumnResponse = skyflowClient.vault("").get(getByColumnRequest); // Replace with actual Vault ID + System.out.println(getByColumnResponse); + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the retrieval process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +### Get by skyflow IDs + +Retrieve specific records using `skyflow_ids`. Ideal for fetching exact records when IDs are known. + +#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/GetExample.java) of a get call to retrieve data using Redaction type: + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to retrieve data from the Skyflow vault using a list of Skyflow IDs. + * + * 1. Initializes the Skyflow client with a given vault ID. + * 2. Creates a request to retrieve records based on Skyflow IDs. + * 3. Specifies that the response should not return tokens. + * 4. Uses plain text redaction type for the retrieved records. + * 5. Prints the response to display the retrieved records. + */ +public class GetExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of Skyflow IDs (replace with actual Skyflow IDs) + ArrayList ids = new ArrayList<>(); + ids.add("a581d205-1969-4350-acbe-a2a13eb871a6"); // Replace with actual Skyflow ID + ids.add("5ff887c3-b334-4294-9acc-70e78ae5164a"); // Replace with actual Skyflow ID + + // Step 2: Create a GetRequest to retrieve records based on Skyflow IDs + // The request specifies: + // - `ids`: The list of Skyflow IDs to retrieve + // - `table`: The table from which the records will be retrieved + // - `returnTokens`: Set to false, meaning tokens will not be returned in the response + // - `redactionType`: Set to PLAIN_TEXT, meaning the retrieved records will have data redacted as plain text + GetRequest getByIdRequest = GetRequest.builder() + .ids(ids) + .table("table1") // Replace with the actual table name + .returnTokens(false) // Set to false to avoid returning tokens + .redactionType(RedactionType.PLAIN_TEXT) // Redact data as plain text + .build(); + + // Step 3: Send the request to the Skyflow vault and retrieve the records + GetResponse getByIdResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getByIdRequest); // Replace with actual Vault ID + System.out.println(getByIdResponse); // Print the response to the console + + } catch (SkyflowException e) { + // Step 4: Handle any errors that occur during the data retrieval process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "data": [ + { + "card_number": "4555555555555553", + "email": "john.doe@gmail.com", + "name": "john doe", + "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" + }, + { + "card_number": "4555555555555559", + "email": "jane.doe@gmail.com", + "name": "jane doe", + "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" + } + ], + "errors": [] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +### Get tokens + +Return tokens for records. Ideal for securely processing sensitive data while maintaining data privacy. + +#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/getExample.java) of get call to retrieve tokens using Skyflow IDs: + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to retrieve data from the Skyflow vault and return tokens along with the records. + * + * 1. Initializes the Skyflow client with a given vault ID. + * 2. Creates a request to retrieve records based on Skyflow IDs and ensures tokens are returned. + * 3. Prints the response to display the retrieved records along with the tokens. + */ +public class GetExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of Skyflow IDs (replace with actual Skyflow IDs) + ArrayList ids = new ArrayList<>(); + ids.add("a581d205-1969-4350-acbe-a2a13eb871a6"); // Replace with actual Skyflow ID + ids.add("5ff887c3-b334-4294-9acc-70e78ae5164a"); // Replace with actual Skyflow ID + + // Step 2: Create a GetRequest to retrieve records based on Skyflow IDs + // The request specifies: + // - `ids`: The list of Skyflow IDs to retrieve + // - `table`: The table from which the records will be retrieved + // - `returnTokens`: Set to true, meaning tokens will be included in the response + GetRequest getTokensRequest = GetRequest.builder() + .ids(ids) + .table("table1") // Replace with the actual table name + .returnTokens(true) // Set to true to include tokens in the response + .build(); + + // Step 3: Send the request to the Skyflow vault and retrieve the records with tokens + GetResponse getTokensResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getTokensRequest); // Replace with actual Vault ID + System.out.println(getTokensResponse); // Print the response to the console + + } catch (SkyflowException e) { + // Step 4: Handle any errors that occur during the data retrieval process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "data": [ + { + "card_number": "3998-2139-0328-0697", + "email": "c9a6c9555060@82c092e7.bd52", + "name": "82c092e7-74c0-4e60-bd52-c9a6c9555060", + "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" + }, + { + "card_number": "3562-0140-8820-7499", + "email": "6174366e2bc6@59f82e89.93fc", + "name": "59f82e89-138e-4f9b-93fc-6174366e2bc6", + "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" + } + ], + "errors": [] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +### Get By column name and column values + +Retrieve records by unique column values. Ideal for querying data without knowing Skyflow IDs, using alternate unique identifiers. + +#### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/GetExample.java) of get call to retrieve data using column name and column values: + +```java +import com.skyflow.enums.RedactionType; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.GetResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to retrieve data from the Skyflow vault based on column values. + * + * 1. Initializes the Skyflow client with a given vault ID. + * 2. Creates a request to retrieve records based on specific column values (e.g., email addresses). + * 3. Prints the response to display the retrieved records after redacting sensitive data based on the specified redaction type. + */ +public class GetExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Initialize a list of column values (email addresses in this case) + ArrayList columnValues = new ArrayList<>(); + columnValues.add("john.doe@gmail.com"); // Example email address + columnValues.add("jane.doe@gmail.com"); // Example email address + + // Step 2: Create a GetRequest to retrieve records based on column values + // The request specifies: + // - `table`: The table from which the records will be retrieved + // - `columnName`: The column to filter the records by (e.g., "email") + // - `columnValues`: The list of values to match in the specified column + // - `redactionType`: Defines how sensitive data should be redacted (set to PLAIN_TEXT here) + GetRequest getByColumnRequest = GetRequest.builder() + .table("table1") // Replace with the actual table name + .columnName("email") // The column name to filter by (e.g., "email") + .columnValues(columnValues) // The list of column values to match + .redactionType(RedactionType.PLAIN_TEXT) // Set the redaction type (e.g., PLAIN_TEXT) + .build(); + + // Step 3: Send the request to the Skyflow vault and retrieve the records + GetResponse getByColumnResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").get(getByColumnRequest); // Replace with actual Vault ID + System.out.println(getByColumnResponse); // Print the response to the console + + } catch (SkyflowException e) { + // Step 4: Handle any errors that occur during the data retrieval process + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "data": [ + { + "card_number": "4555555555555553", + "email": "john.doe@gmail.com", + "name": "john doe", + "skyflowId": "a581d205-1969-4350-acbe-a2a13eb871a6" + }, + { + "card_number": "4555555555555559", + "email": "jane.doe@gmail.com", + "name": "jane doe", + "skyflowId": "5ff887c3-b334-4294-9acc-70e78ae5164a" + } + ], + "errors": [] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +### Redaction types + +See [`RedactionType`](../docs/api_reference.md#redactiontype) in the API Reference for all available values and their descriptions. + +## Update + +To update data in your vault, use the `update` method. [`UpdateRequest`](../docs/api_reference.md#updaterequest) accepts the table name, data map, optional tokens, `returnTokens`, and `tokenMode`. Returns an [`UpdateResponse`](../docs/api_reference.md#updateresponse) with the `skyflow_id` and (when `returnTokens=true`) a token per updated column. + +### Construct an update request + +```java +import com.skyflow.enums.TokenMode; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateResponse; + +import java.util.HashMap; + +/** + * This example demonstrates how to update records in the Skyflow vault by providing new data and/or tokenized values, along with corresponding UpdateRequest schema. + * + */ +public class UpdateSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Prepare the data to update in the vault + // Use a HashMap to store the data that will be updated in the specified table + HashMap data = new HashMap<>(); + data.put("skyflow_id", ""); // Skyflow ID for identifying the record to update + data.put("", ""); // Example of a column name and its value to update + data.put("", ""); // Another example of a column name and its value to update + + // Step 2: Prepare the tokens (if necessary) for certain columns that require tokenization + // Use a HashMap to specify columns that need tokens in the update request + HashMap tokens = new HashMap<>(); + tokens.put("", ""); // Example of a column name that should be tokenized + + // Step 3: Create an UpdateRequest to specify the update operation + // The request includes the table name, token mode, data, tokens, and the returnTokens flag + UpdateRequest updateRequest = UpdateRequest.builder() + .table("") // Replace with the actual table name to update + .tokenMode(TokenMode.ENABLE) // Specifies the tokenization mode (ENABLE means tokenization is applied) + .data(data) // The data to update in the record + .tokens(tokens) // The tokens associated with specific columns + .returnTokens(true) // Specify whether to return tokens in the response + .build(); + + // Step 4: Send the request to the Skyflow vault and update the record + UpdateResponse updateResponse = skyflowClient.vault("").update(updateRequest); // Replace with actual Vault ID + System.out.println(updateResponse); // Print the response to confirm the update result + + } catch (SkyflowException e) { + // Step 5: Handle any errors that occur during the update operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/UpdateExample.java) of update call + +```java +import com.skyflow.enums.TokenMode; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.data.UpdateResponse; + +import java.util.HashMap; + +/** + * This example demonstrates how to update a record in the Skyflow vault with specified data and tokens. + * + * 1. Initializes the Skyflow client with a given vault ID. + * 2. Constructs an update request with data to modify and tokens to include. + * 3. Sends the request to update the record in the vault. + * 4. Prints the response to confirm the success or failure of the update operation. + */ +public class UpdateExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Prepare the data to update in the vault + // A HashMap is used to store the data that will be updated in the specified table + HashMap data = new HashMap<>(); + data.put("skyflow_id", "5b699e2c-4301-4f9f-bcff-0a8fd3057413"); // Skyflow ID identifies the record to update + data.put("name", "john doe"); // Updating the "name" column with a new value + data.put("card_number", "4111111111111115"); // Updating the "card_number" column with a new value + + // Step 2: Prepare the tokens to include in the update request + // Tokens can be included to update sensitive data with tokenized values + HashMap tokens = new HashMap<>(); + tokens.put("name", "72b8ffe3-c8d3-4b4f-8052-38b2a7405b5a"); // Tokenized value for the "name" column + + // Step 3: Create an UpdateRequest to define the update operation + // The request specifies the table name, token mode, data, and tokens for the update + UpdateRequest updateRequest = UpdateRequest.builder() + .table("table1") // Replace with the actual table name to update + .tokenMode(TokenMode.ENABLE) // Token mode enabled to allow tokenization of sensitive data + .data(data) // The data to update in the record + .tokens(tokens) // The tokenized values for sensitive columns + .build(); + + // Step 4: Send the update request to the Skyflow vault + UpdateResponse updateResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").update(updateRequest); // Replace with your actual Vault ID + System.out.println(updateResponse); // Print the response to confirm the update result + + } catch (SkyflowException e) { + // Step 5: Handle any exceptions that occur during the update operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging purposes + } + } +} +``` + +Sample response: + +- When `returnTokens` is set to `true` + +```json +{ + "skyflowId": "5b699e2c-4301-4f9f-bcff-0a8fd3057413", + "name": "72b8ffe3-c8d3-4b4f-8052-38b2a7405b5a", + "card_number": "4315-7650-1359-9681" +} +``` + +- When `returnTokens` is set to `false` + +```json +{ + "skyflowId": "5b699e2c-4301-4f9f-bcff-0a8fd3057413" +} +``` + +## Delete + +To delete records using Skyflow IDs, use the `delete` method. [`DeleteRequest`](../docs/api_reference.md#deleterequest) accepts a table name and list of Skyflow IDs. Returns a [`DeleteResponse`](../docs/api_reference.md#deleteresponse). + +### Construct a delete request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to delete records from a Skyflow vault using specified Skyflow IDs, along with corresponding DeleteRequest schema. + * + */ +public class DeleteSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Prepare a list of Skyflow IDs for the records to delete + // The list stores the Skyflow IDs of the records that need to be deleted from the vault + ArrayList ids = new ArrayList<>(); + ids.add(""); // Replace with actual Skyflow ID 1 + ids.add(""); // Replace with actual Skyflow ID 2 + ids.add(""); // Replace with actual Skyflow ID 3 + + // Step 2: Create a DeleteRequest to define the delete operation + // The request specifies the table from which to delete the records and the IDs of the records to delete + DeleteRequest deleteRequest = DeleteRequest.builder() + .ids(ids) // List of Skyflow IDs to delete + .table("") // Replace with the actual table name from which to delete + .build(); + + // Step 3: Send the delete request to the Skyflow vault + DeleteResponse deleteResponse = skyflowClient.vault("").delete(deleteRequest); // Replace with your actual Vault ID + System.out.println(deleteResponse); // Print the response to confirm the delete result + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the delete operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging purposes + } + } +} +``` + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/DeleteExample.java) of delete call: + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.DeleteResponse; + +import java.util.ArrayList; + +/** + * This example demonstrates how to delete records from a Skyflow vault using specified Skyflow IDs. + * + * 1. Initializes the Skyflow client with a given Vault ID. + * 2. Constructs a delete request by specifying the IDs of the records to delete. + * 3. Sends the delete request to the Skyflow vault to delete the specified records. + * 4. Prints the response to confirm the success or failure of the delete operation. + */ +public class DeleteExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Prepare a list of Skyflow IDs for the records to delete + // The list stores the Skyflow IDs of the records that need to be deleted from the vault + ArrayList ids = new ArrayList<>(); + ids.add("9cbf66df-6357-48f3-b77b-0f1acbb69280"); // Replace with actual Skyflow ID 1 + ids.add("ea74bef4-f27e-46fe-b6a0-a28e91b4477b"); // Replace with actual Skyflow ID 2 + ids.add("47700796-6d3b-4b54-9153-3973e281cafb"); // Replace with actual Skyflow ID 3 + + // Step 2: Create a DeleteRequest to define the delete operation + // The request specifies the table from which to delete the records and the IDs of the records to delete + DeleteRequest deleteRequest = DeleteRequest.builder() + .ids(ids) // List of Skyflow IDs to delete + .table("table1") // Replace with the actual table name from which to delete + .build(); + + // Step 3: Send the delete request to the Skyflow vault + DeleteResponse deleteResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").delete(deleteRequest); // Replace with your actual Vault ID + System.out.println(deleteResponse); // Print the response to confirm the delete result + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the delete operation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging purposes + } + } +} +``` + +Sample response: + +```json +{ + "deletedIds": [ + "9cbf66df-6357-48f3-b77b-0f1acbb69280", + "ea74bef4-f27e-46fe-b6a0-a28e91b4477b", + "47700796-6d3b-4b54-9153-3973e281cafb" + ] +} +``` + +## Query + +To retrieve data with SQL queries, use the `query` method. [`QueryRequest`](../docs/api_reference.md#queryrequest) accepts a `query` string. Returns a [`QueryResponse`](../docs/api_reference.md#queryresponse). + +### Construct a query request + +Refer to [Query your data](https://docs.skyflow.com/query-data/) and [Execute Query](https://docs.skyflow.com/record/#QueryService_ExecuteQuery) for guidelines and restrictions on supported SQL statements, operators, and keywords. + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; + +/** + * This example demonstrates how to execute a custom SQL query on a Skyflow vault, along with QueryRequest schema. + * + */ +public class QuerySchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Define the SQL query to execute on the Skyflow vault + // Replace "" with the actual SQL query you want to run + String query = ""; // Example: "SELECT * FROM table1 WHERE column1 = 'value'" + + // Step 2: Create a QueryRequest with the specified SQL query + QueryRequest queryRequest = QueryRequest.builder() + .query(query) // SQL query to execute + .build(); + + // Step 3: Execute the query request on the specified Skyflow vault + QueryResponse queryResponse = skyflowClient.vault("").query(queryRequest); // Replace with your actual Vault ID + System.out.println(queryResponse); // Print the response containing the query results + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the query execution + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging + } + } +} +``` + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/QueryExample.java) of query call + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.QueryResponse; + +/** + * This example demonstrates how to execute a SQL query on a Skyflow vault to retrieve data. + * + * 1. Initializes the Skyflow client with the Vault ID. + * 2. Constructs a query request with a specified SQL query. + * 3. Executes the query against the Skyflow vault. + * 4. Prints the response from the query execution. + */ +public class QueryExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Define the SQL query + // Example query: Retrieve all records from the "cards" table with a specific skyflow_id + String query = "SELECT * FROM cards WHERE skyflow_id='3ea3861-x107-40w8-la98-106sp08ea83f'"; + + // Step 2: Create a QueryRequest with the SQL query + QueryRequest queryRequest = QueryRequest.builder() + .query(query) // SQL query to execute + .build(); + + // Step 3: Execute the query request on the specified Skyflow vault + QueryResponse queryResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").query(queryRequest); // Vault ID: 9f27764a10f7946fe56b3258e117 + System.out.println(queryResponse); // Print the query response (contains query results) + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the query execution + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging + } + } +} +``` + +Sample response: + +```json +{ + "fields": [ + { + "card_number": "XXXXXXXXXXXX1112", + "name": "S***ar", + "skyflowId": "3ea3861-x107-40w8-la98-106sp08ea83f", + "tokenizedData": null + } + ] +} +``` + +> **Note:** The response key is `skyflowId`. The legacy `skyflow_id` key is deprecated and will be removed in an upcoming release. + +## Upload File + +To upload files to a Skyflow vault, use the `uploadFile` method. [`FileUploadRequest`](../docs/api_reference.md#fileuploadrequest) accepts the table name, column name, optional skyflow ID, and a file source (`fileObject`, `filePath`, or `base64`). Returns a [`FileUploadResponse`](../docs/api_reference.md#fileuploadresponse). + +### Construct a file upload request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.FileUploadRequest; +import com.skyflow.vault.data.FileUploadResponse; + +/** + * This example demonstrates how to upload a file to a Skyflow vault, along with the UploadFileRequest schema. + * + */ +public class UploadFileSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Specify file Object + File file = new File(""); + + // Step 2: Create an UploadFileRequest with the file details + FileUploadRequest uploadFileRequest = FileUploadRequest.builder() + .fileObject(file) // File object + .table("") // Vault table to upload into + .columnName("") // Column to assign to the uploaded file + .skyflowId("") // Skyflow id of the record + .build(); + + // Step 3: Execute the file upload request on the specified Skyflow vault + FileUploadResponse fileUploadResponse = skyflowClient.vault().uploadFile(uploadFileRequest); + System.out.println("File Upload Response: " + fileUploadResponse); + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions that occur during the upload + System.out.println("Error occurred during file upload:"); + e.printStackTrace(); // Print the exception stack trace for debugging + } + } +} + +``` + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/vault/FileUploadExample.java) of file upload call +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.data.FileUploadRequest; +import com.skyflow.vault.data.FileUploadResponse; + +/** + * This example demonstrates how to upload a file to a Skyflow vault. + * + * 1. Initializes the Skyflow client with the Vault ID. + * 2. Constructs a file upload request with the file path, table name, and file name. + * 3. Executes the upload request against the Skyflow vault. + * 4. Prints the response from the upload. + */ +public class UploadFileExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Specify file Object + File file = new File("test/sample.txt"); + + // Step 2: Create an UploadFileRequest with the file details + FileUploadRequest uploadFileRequest = FileUploadRequest.builder() + .fileObject(file) // File object + .table("cards") // Vault table to upload into + .columnName("file") // Column to assign to the uploaded file + .skyflowId("c9312531-2087-439a-bd26-74c41f24db83") // Skyflow id of the record + .build(); + + // Step 3: Execute the file upload request + FileUploadResponse uploadResponse = skyflowClient.vault("9f27764a10f7946fe56b3258e117").uploadFile(uploadFileRequest); + System.out.println("File Upload Response: " + fileUploadResponse); + + } catch (SkyflowException e) { + // Step 4: Handle any exceptions during the upload + System.out.println("Error occurred during file upload:"); + e.printStackTrace(); // Print exception details for debugging + } + } +} + +``` + +Sample response: + +```json +{ + "skyflowId": "c9312531-2087-439a-bd26-74c41f24db83", + "errors": null +} +``` + +# Detect +Skyflow Detect enables you to deidentify and reidentify sensitive data in text and files, supporting advanced privacy-preserving workflows. + +`DetectController` is the class returned by `skyflowClient.detect()` and `skyflowClient.detect(vaultId)`. + +```java +// Uses the default (first configured) vault +DetectController detect = skyflowClient.detect(); + +// Uses a specific vault by ID +DetectController detect = skyflowClient.detect(""); +``` + +**Methods:** + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `deidentifyText(DeidentifyTextRequest)` | [`DeidentifyTextRequest`](../docs/api_reference.md#deidentifytextrequest) | [`DeidentifyTextResponse`](../docs/api_reference.md#deidentifytextresponse) | Deidentify sensitive entities in text | +| `reidentifyText(ReidentifyTextRequest)` | [`ReidentifyTextRequest`](../docs/api_reference.md#reidentifytextrequest) | [`ReidentifyTextResponse`](../docs/api_reference.md#reidentifytextresponse) | Restore original values from a deidentified text | +| `deidentifyFile(DeidentifyFileRequest)` | [`DeidentifyFileRequest`](../docs/api_reference.md#deidentifyfilerequest) | [`DeidentifyFileResponse`](../docs/api_reference.md#deidentifyfileresponse) | Deidentify sensitive data in a file | +| `getDetectRun(GetDetectRunRequest)` | [`GetDetectRunRequest`](../docs/api_reference.md#getdetectrunrequest) | [`DeidentifyFileResponse`](../docs/api_reference.md#deidentifyfileresponse) | Poll for the result of an async file deidentification | + +## Deidentify Text +To deidentify text, use the `deidentifyText` method. [`DeidentifyTextRequest`](../docs/api_reference.md#deidentifytextrequest) accepts the text to deidentify along with optional entity types, regex lists, token format, and transformations. Returns a [`DeidentifyTextResponse`](../docs/api_reference.md#deidentifytextresponse). + +### Construct an deidentify text request + +```java +import com.skyflow.enums.DetectEntities; +import com.skyflow.vault.detect.DateTransformation; +import com.skyflow.vault.detect.DeidentifyTextRequest; +import com.skyflow.vault.detect.TokenFormat; +import com.skyflow.vault.detect.Transformations; +import com.skyflow.vault.detect.DeidentifyTextResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + * This example demonstrate to build deidentify text request. + */ +public class DeidentifyTextSchema { + + public static void main(String[] args) { + + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Configure the options for deidentify text + + // Replace with the entity you want to detect + List detectEntitiesList = new ArrayList<>(); + detectEntitiesList.add(DetectEntities.SSN); + + // Replace with the entity you want to detect with vault token + List vaultTokenList = new ArrayList<>(); + vaultTokenList.add(DetectEntities.CREDIT_CARD); + + // Replace with the entity you want to detect with entity only + List entityOnlyList = new ArrayList<>(); + entityOnlyList.add(DetectEntities.SSN); + + // Replace with the entity you want to detect with entity unique counter + List entityUniqueCounterList = new ArrayList<>(); + entityUniqueCounterList.add(DetectEntities.SSN); + + // Replace with the regex patterns you want to allow during deidentification + List allowRegexList = new ArrayList<>(); + allowRegexList.add(""); + + // Replace with the regex patterns you want to restrict during deidentification + List restrictRegexList = new ArrayList<>(); + restrictRegexList.add("YOUR_RESTRICT_REGEX_LIST"); + + // Configure Token Format + TokenFormat tokenFormat = TokenFormat.builder() + .vaultToken(vaultTokenList) + .entityOnly(entityOnlyList) + .entityUniqueCounter(entityUniqueCounterList) + .build(); + + // Configure Transformation + List detectEntitiesTransformationList = new ArrayList<>(); + detectEntitiesTransformationList.add(DetectEntities.DOB); // Replace with the entity you want to transform + + DateTransformation dateTransformation = new DateTransformation(20, 5, detectEntitiesTransformationList); + Transformations transformations = new Transformations(dateTransformation); + + // Step 3: Create a deidentify text request for the vault + DeidentifyTextRequest deidentifyTextRequest = DeidentifyTextRequest.builder() + .text("") // Replace with the text you want to deidentify + .entities(detectEntitiesList) + .allowRegexList(allowRegexList) + .restrictRegexList(restrictRegexList) + .tokenFormat(tokenFormat) + .transformations(transformations) + .build(); + + // Step 4: Use the Skyflow client to perform the deidentifyText operation + // Replace with your actual vault ID + DeidentifyTextResponse deidentifyTextResponse = skyflowClient.detect("").deidentifyText(deidentifyTextRequest); + + // Step 5: Print the response + System.out.println("Deidentify text Response: " + deidentifyTextResponse); + } +} + +``` + +## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/DeidentifyTextExample.java) of deidentify text: +```java +import java.util.ArrayList; +import java.util.List; + +import com.skyflow.enums.DetectEntities; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.detect.DateTransformation; +import com.skyflow.vault.detect.DeidentifyTextRequest; +import com.skyflow.vault.detect.DeidentifyTextResponse; +import com.skyflow.vault.detect.TokenFormat; +import com.skyflow.vault.detect.Transformations; + +/** + * Skyflow Deidentify Text Example + *

+ * This example demonstrates how to use the Skyflow SDK to deidentify text data + * across multiple vaults. It includes: + * 1. Setting up credentials and vault configurations. + * 2. Creating a Skyflow client with multiple vaults. + * 3. Performing deidentify of text with various options. + * 4. Handling responses and errors. + */ + +public class DeidentifyTextExample { + public static void main(String[] args) throws SkyflowException { + + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Configuring the different options for deidentify + + // Replace with the entity you want to detect + List detectEntitiesList = new ArrayList<>(); + detectEntitiesList.add(DetectEntities.SSN); + detectEntitiesList.add(DetectEntities.CREDIT_CARD); + + // Replace with the entity you want to detect with vault token + List vaultTokenList = new ArrayList<>(); + vaultTokenList.add(DetectEntities.SSN); + vaultTokenList.add(DetectEntities.CREDIT_CARD); + + // Configure Token Format + TokenFormat tokenFormat = TokenFormat.builder() + .vaultToken(vaultTokenList) + .build(); + + // Configure Transformation for deidentified entities + List detectEntitiesTransformationList = new ArrayList<>(); + detectEntitiesTransformationList.add(DetectEntities.DOB); // Replace with the entity you want to transform + + DateTransformation dateTransformation = new DateTransformation(20, 5, detectEntitiesTransformationList); + Transformations transformations = new Transformations(dateTransformation); + + // Step 3: invoking Deidentify text on the vault + try { + // Create a deidentify text request for the vault + DeidentifyTextRequest deidentifyTextRequest = DeidentifyTextRequest.builder() + .text("My SSN is 123-45-6789 and my card is 4111 1111 1111 1111.") // Replace with your deidentify text + .entities(detectEntitiesList) + .tokenFormat(tokenFormat) + .transformations(transformations) + .build(); + // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id + DeidentifyTextResponse deidentifyTextResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").deidentifyText(deidentifyTextRequest); + + System.out.println("Deidentify text Response: " + deidentifyTextResponse); + } catch (SkyflowException e) { + System.err.println("Error occurred during deidentify: "); + e.printStackTrace(); // Print the exception for debugging purposes + } + } +} +``` + +Sample Response: +```json +{ + "processedText": "My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].", + "entities": [ + { + "token": "SSN_IWdexZe", + "value": "123-45-6789", + "textIndex": { + "start": 10, + "end": 21 + }, + "processedIndex": { + "start": 10, + "end": 23 + }, + "entity": "SSN", + "scores": { + "SSN": 0.9384 + } + }, + { + "token": "CREDIT_CARD_rUzMjdQ", + "value": "4111 1111 1111 1111", + "textIndex": { + "start": 37, + "end": 56 + }, + "processedIndex": { + "start": 39, + "end": 60 + }, + "entity": "CREDIT_CARD", + "scores": { + "CREDIT_CARD": 0.9051 + } + } + ], + "wordCount": 9, + "charCount": 57 +} +``` + +## Reidentify Text +To reidentify text, use the `reidentifyText` method. [`ReidentifyTextRequest`](../docs/api_reference.md#reidentifytextrequest) accepts the redacted/deidentified text and optional entity lists controlling which entities to reveal, mask, or keep redacted. Returns a [`ReidentifyTextResponse`](../docs/api_reference.md#reidentifytextresponse). + +### Construct an reidentify text request + +```java +import com.skyflow.enums.DetectEntities; +import com.skyflow.vault.detect.ReidentifyTextRequest; +import com.skyflow.vault.detect.ReidentifyTextResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + * This example demonstrates how to build a reidentify text request. + */ +public class ReidentifyTextSchema { + public static void main(String[] args) { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Configuring the different options for reidentify + List maskedEntity = new ArrayList<>(); + maskedEntity.add(DetectEntities.CREDIT_CARD); // Replace with the entity you want to mask + + List plainTextEntity = new ArrayList<>(); + plainTextEntity.add(DetectEntities.SSN); // Replace with the entity you want to keep in plain text + + // List redactedEntity = new ArrayList<>(); + // redactedEntity.add(DetectEntities.SSN); // Replace with the entity you want to redact + + + // Step 3: Create a reidentify text request with the configured entities + ReidentifyTextRequest reidentifyTextRequest = ReidentifyTextRequest.builder() + .text("My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].") // Replace with your deidentify text + .maskedEntities(maskedEntity) +// .redactedEntities(redactedEntity) + .plainTextEntities(plainTextEntity) + .build(); + + // Step 4: Invoke reidentify text on the vault + ReidentifyTextResponse reidentifyTextResponse = skyflowClient.detect("").reidentifyText(reidentifyTextRequest); + System.out.println("Reidentify text Response: " + reidentifyTextResponse); + } +} +``` + +## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/ReidentifyTextExample.java) of Reidentify text + +```java +import com.skyflow.enums.DetectEntities; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.detect.ReidentifyTextRequest; +import com.skyflow.vault.detect.ReidentifyTextResponse; + +import java.util.ArrayList; +import java.util.List; + +/** + * Skyflow Reidentify Text Example + *

+ * This example demonstrates how to use the Skyflow SDK to reidentify text data + * across multiple vaults. It includes: + * 1. Setting up credentials and vault configurations. + * 2. Creating a Skyflow client with multiple vaults. + * 3. Performing reidentify of text with various options. + * 4. Handling responses and errors. + */ + +public class ReidentifyTextExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Configuring the different options for reidentify + List maskedEntity = new ArrayList<>(); + maskedEntity.add(DetectEntities.CREDIT_CARD); // Replace with the entity you want to mask + + List plainTextEntity = new ArrayList<>(); + plainTextEntity.add(DetectEntities.SSN); // Replace with the entity you want to keep in plain text + + try { + // Step 3: Create a reidentify text request with the configured options + ReidentifyTextRequest reidentifyTextRequest = ReidentifyTextRequest.builder() + .text("My SSN is [SSN_IWdexZe] and my card is [CREDIT_CARD_rUzMjdQ].") // Replace with your deidentify text + .maskedEntities(maskedEntity) + .plainTextEntities(plainTextEntity) + .build(); + + // Step 4: Invoke Reidentify text on the vault + // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id + ReidentifyTextResponse reidentifyTextResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").reidentifyText(reidentifyTextRequest); + + // Handle the response from the reidentify text request + System.out.println("Reidentify text Response: " + reidentifyTextResponse); + } catch (SkyflowException e) { + System.err.println("Error occurred during reidentify : "); + e.printStackTrace(); + } + } +} +``` + +Sample Response: + +```json +{ + "processedText":"My SSN is 123-45-6789 and my card is XXXXX1111." +} +``` + +## Deidentify file +To deidentify files, use the `deidentifyFile` method. [`DeidentifyFileRequest`](../docs/api_reference.md#deidentifyfilerequest) accepts a [`FileInput`](../docs/api_reference.md#fileinput) and optional parameters controlling entity detection, masking, output format, and async wait time. Supports images, PDFs, audio, documents, spreadsheets, and presentations. Returns a [`DeidentifyFileResponse`](../docs/api_reference.md#deidentifyfileresponse). + +### AudioBleep + +[`AudioBleep`](../docs/api_reference.md#audiobleep) controls how detected sensitive audio segments are replaced with a bleep tone. Used in `DeidentifyFileRequest.builder().bleep(audioBleep)` for audio files. + +```java +import com.skyflow.vault.detect.AudioBleep; + +AudioBleep audioBleep = AudioBleep.builder() + .frequency(1000D) // bleep tone frequency in Hz + .gain(0.5D) // bleep tone gain (volume level) + .startPadding(0.2D) // silence padding before the bleep (seconds) + .stopPadding(0.2D) // silence padding after the bleep (seconds) + .build(); +``` + +### Construct an deidentify file request + +```java +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.LogLevel; +import com.skyflow.enums.MaskingMethod; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.detect.DeidentifyFileRequest; +import com.skyflow.vault.detect.DeidentifyFileResponse; + +import java.io.File; + +/** + * This example demonstrates how to build a deidentify file request. + */ + +public class DeidentifyFileSchema { + + public static void main(String[] args) { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Create a deidentify file request with all options + + // Create file object + File file = new File(""); // Replace with the path to the file you want to deidentify + + // Create file input using the file object + FileInput fileInput = FileInput.builder() + .file(file) + // .filePath("") // Alternatively, you can use .filePath() + .build(); + + // Output configuration + String outputDirectory = ""; // Replace with the desired output directory to save the deidentified file + + // Entities to detect + // List detectEntities = new ArrayList<>(); + // detectEntities.add(DetectEntities.IP_ADDRESS); // Replace with the entities you want to detect + + // Image-specific options + // Boolean outputProcessedImage = true; // Include processed image in output + // Boolean outputOcrText = true; // Include OCR text in output + MaskingMethod maskingMethod = MaskingMethod.BLACKBOX; // Masking method for images + + // PDF-specific options + // Integer pixelDensity = 15; // Pixel density for PDF processing + // Integer maxResolution = 2000; // Max resolution for PDF + + // Audio-specific options + // Boolean outputProcessedAudio = true; // Include processed audio + // DetectOutputTranscriptions outputTanscription = DetectOutputTranscriptions.PLAINTEXT_TRANSCRIPTION; // Transcription type + + // Audio bleep configuration + // AudioBleep audioBleep = AudioBleep.builder() + // .frequency(5D) // Pitch in Hz + // .startPadding(7D) // Padding at start (seconds) + // .stopPadding(8D) // Padding at end (seconds) + // .build(); + + Integer waitTime = 20; // Max wait time for response (max 64 seconds) + + DeidentifyFileRequest deidentifyFileRequest = DeidentifyFileRequest.builder() + .file(fileInput) + .waitTime(waitTime) + .entities(detectEntities) + .outputDirectory(outputDirectory) + .maskingMethod(maskingMethod) + // .outputProcessedImage(outputProcessedImage) + // .outputOcrText(outputOcrText) + // .pixelDensity(pixelDensity) + // .maxResolution(maxResolution) + // .outputProcessedAudio(outputProcessedAudio) + // .outputTranscription(outputTanscription) + // .bleep(audioBleep) + .build(); + + + DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("").deidentifyFile(deidentifyFileRequest); + System.out.println("Deidentify file response: " + deidentifyFileResponse.toString()); + } +} +``` + +## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/DeidentifyFileExample.java) of Deidentify file + +```java +import java.io.File; + +import com.skyflow.enums.MaskingMethod; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.detect.DeidentifyFileRequest; +import com.skyflow.vault.detect.DeidentifyFileResponse; + +/** + * Skyflow Deidentify File Example + *

+ * This example demonstrates how to use the Skyflow SDK to deidentify file + * It has all available options for deidentifying files. + * Supported file types: images (jpg, png, etc.), pdf, audio (mp3, wav), documents, spreadsheets, presentations, structured text. + * It includes: + * 1. Configure credentials + * 2. Set up vault configuration + * 3. Create a deidentify file request with all options + * 4. Call deidentifyFile to deidentify file. + * 5. Handle response and errors + */ +public class DeidentifyFileExample { + + public static void main(String[] args) throws SkyflowException { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + try { + // Step 2: Create a deidentify file request with all options + + + // Create file object + File file = new File("sensitive-folder/personal-info.txt"); // Replace with the path to the file you want to deidentify + + // Create file input using the file object + FileInput fileInput = FileInput.builder() + .file(file) + // .filePath("") // Alternatively, you can use .filePath() + .build(); + + // Output configuration + String outputDirectory = "deidentified-file/"; // Replace with the desired output directory to save the deidentified file + + // Entities to detect + // List detectEntities = new ArrayList<>(); + // detectEntities.add(DetectEntities.IP_ADDRESS); // Replace with the entities you want to detect + + // Image-specific options + // Boolean outputProcessedImage = true; // Include processed image in output + // Boolean outputOcrText = true; // Include OCR text in output + MaskingMethod maskingMethod = MaskingMethod.BLACKBOX; // Masking method for images + + Integer waitTime = 20; // Max wait time for response (max 64 seconds) + + DeidentifyFileRequest deidentifyFileRequest = DeidentifyFileRequest.builder() + .file(fileInput) + .waitTime(waitTime) + .outputDirectory(outputDirectory) + .maskingMethod(maskingMethod) + .build(); + + // Step 3: Invoking deidentifyFile + // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id + DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").deidentifyFile(deidentifyFileRequest); + System.out.println("Deidentify file response: " + deidentifyFileResponse.toString()); + } catch (SkyflowException e) { + System.err.println("Error occurred during deidentify file: "); + e.printStackTrace(); + } + } +} + +``` + +Sample response: + +```json +{ + "file": { + "name": "deidentified.txt", + "size": 33, + "type": "", + "lastModified": 1751355183039 + }, + "fileBase64": "bXkgY2FyZCBudW1iZXIgaXMgW0NSRURJVF", + "type": "redacted_file", + "extension": "txt", + "wordCount": 11, + "charCount": 61, + "sizeInKb": 0, + "entities": [ + { + "file": "bmFtZTogW05BTUVfMV0gCm==", + "type": "entities", + "extension": "json" + } + ], + "runId": "undefined", + "status": "success" +} + +``` + +**Supported file types:** +- Documents: `doc`, `docx`, `pdf` +- PDFs: `pdf` +- Images: `bmp`, `jpeg`, `jpg`, `png`, `tif`, `tiff` +- Structured text: `json`, `xml` +- Spreadsheets: `csv`, `xls`, `xlsx` +- Presentations: `ppt`, `pptx` +- Audio: `mp3`, `wav` + +**Note:** +- Transformations cannot be applied to Documents, Images, or PDFs file formats. + +- The `waitTime` option must be ≤ 64 seconds; otherwise, an error is thrown. + +- If the API takes more than 64 seconds to process the file, it will return only the run ID in the response. + +Sample response (when the API takes more than 64 seconds): +```json +{ + "file": null, + "fileBase64": null, + "type": null, + "extension": null, + "wordCount": null, + "charCount": null, + "sizeInKb": null, + "durationInSeconds": null, + "pageCount": null, + "slideCount": null, + "entities": null, + "runId": "1273a8c6-c498-4293-a9d6-389864cd3a44", + "status": "IN_PROGRESS", + "errors": null +} +``` + +## Get run: +To retrieve the results of a previously started file deidentification operation, use the `getDetectRun` method. [`GetDetectRunRequest`](../docs/api_reference.md#getdetectrunrequest) accepts the `runId` returned from a prior `deidentifyFile` call. Returns a [`DeidentifyFileResponse`](../docs/api_reference.md#deidentifyfileresponse). + +### Construct an get run request + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.detect.DeidentifyFileResponse; +import com.skyflow.vault.detect.GetDetectRunRequest; + +/** + * Skyflow Get Detect Run Example + */ + +public class GetDetectRunSchema { + + public static void main(String[] args) { + try { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + + // Step 2: Create a get detect run request + GetDetectRunRequest getDetectRunRequest = GetDetectRunRequest.builder() + .runId("") // Replace with the runId from deidentifyFile call + .build(); + + // Step 3: Call getDetectRun to poll for file processing results + // Replace with your actual vault ID + DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("").getDetectRun(getDetectRunRequest); + System.out.println("Get Detect Run Response: " + deidentifyFileResponse); + } catch (SkyflowException e) { + System.err.println("Error occurred during get detect run: "); + e.printStackTrace(); + } + } +} + +``` + +## An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/detect/GetDetectRunExample.java) of get run +```java +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.detect.DeidentifyFileResponse; +import com.skyflow.vault.detect.GetDetectRunRequest; + +/** + * Skyflow Get Detect Run Example + *

+ * This example demonstrates how to: + * 1. Configure credentials + * 2. Set up vault configuration + * 3. Create a get detect run request + * 4. Call getDetectRun to poll for file processing results + * 5. Handle response and errors + */ +public class GetDetectRunExample { + public static void main(String[] args) throws SkyflowException { + // Step 1: Initialize the Skyflow client by configuring the credentials & vault config. + try { + + // Step 2: Create a get detect run request + GetDetectRunRequest getDetectRunRequest = GetDetectRunRequest.builder() + .runId("e0038196-4a20-422b-bad7-e0477117f9bb") // Replace with the runId from deidentifyFile call + .build(); + + // Step 3: Call getDetectRun to poll for file processing results + // Replace `9f27764a10f7946fe56b3258e117` with the actual vault id + DeidentifyFileResponse deidentifyFileResponse = skyflowClient.detect("9f27764a10f7946fe56b3258e117").getDetectRun(getDetectRunRequest); + System.out.println("Get Detect Run Response: " + deidentifyFileResponse); + } catch (SkyflowException e) { + System.err.println("Error occurred during get detect run: "); + e.printStackTrace(); + } + } +} +``` + +Sample Response: + +```json +{ + "file": "bmFtZTogW05BTET0JfMV0K", + "type": "redacted_file", + "extension": "txt", + "wordCount": 11, + "charCount": 61, + "sizeInKb": 0.0, + "entities": [ + { + "file": "gW05BTUVfMV0gCmNhcmQ0K", + "type": "entities", + "extension": "json" + } + ], + "runId": "e0038196-4a20-422b-bad7-e0477117f9bb", + "status": "success" +} + +``` + +## Detect response types + +The Detect API returns structured objects for detected entities. See the API Reference for full attribute lists: [`EntityInfo`](../docs/api_reference.md#entityinfo), [`TextIndex`](../docs/api_reference.md#textindex), [`FileEntityInfo`](../docs/api_reference.md#fileentityinfo), [`FileInfo`](../docs/api_reference.md#fileinfo). + +### EntityInfo and TextIndex + +[`EntityInfo`](../docs/api_reference.md#entityinfo) appears in `DeidentifyTextResponse.getEntities()`. Each entry includes the detected entity type, original value, replacement token, character positions ([`TextIndex`](../docs/api_reference.md#textindex)), and confidence scores. + +```java +DeidentifyTextResponse response = skyflowClient.detect("").deidentifyText(request); + +for (EntityInfo entity : response.getEntities()) { + System.out.println("Entity : " + entity.getEntity()); + System.out.println("Value : " + entity.getValue()); + System.out.println("Token : " + entity.getToken()); + System.out.println("Start : " + entity.getTextIndex().getStart()); + System.out.println("End : " + entity.getTextIndex().getEnd()); + System.out.println("Score : " + entity.getScores().get(entity.getEntity())); +} +``` + +### FileEntityInfo and FileInfo + +[`FileEntityInfo`](../docs/api_reference.md#fileentityinfo) appears in `DeidentifyFileResponse.getEntities()`. [`FileInfo`](../docs/api_reference.md#fileinfo) is returned by `DeidentifyFileResponse.getFile()` and contains file metadata. + +## Detect enums + +See the API Reference for full value descriptions: [`TokenType`](../docs/api_reference.md#tokentype), [`DeidentifyFileStatus`](../docs/api_reference.md#deidentifyfilestatus), [`DetectOutputTranscriptions`](../docs/api_reference.md#detectoutputtranscriptions), [`MaskingMethod`](../docs/api_reference.md#maskingmethod), [`DetectEntities`](../docs/api_reference.md#detectentities). + +### TokenType + +[`TokenType`](../docs/api_reference.md#tokentype) controls how detected entities are tokenized. Used in `TokenFormat.builder()`. + +```java +import com.skyflow.enums.TokenType; + +TokenFormat tokenFormat = TokenFormat.builder() + .vaultToken(vaultTokenList) // uses VAULT_TOKEN + .entityOnly(entityOnlyList) // uses ENTITY_ONLY + .entityUniqueCounter(entityUniqueCounterList) // uses ENTITY_UNIQUE_COUNTER + .build(); +``` + +### DeidentifyFileStatus + +[`DeidentifyFileStatus`](../docs/api_reference.md#deidentifyfilestatus) is returned in `DeidentifyFileResponse.getStatus()` to indicate async processing state. + +```java +import com.skyflow.enums.DeidentifyFileStatus; + +DeidentifyFileResponse response = skyflowClient.detect("").getDetectRun(request); +if (DeidentifyFileStatus.SUCCESS.value().equals(response.getStatus())) { + // safe to read response.getFile() +} else if (DeidentifyFileStatus.IN_PROGRESS.value().equals(response.getStatus())) { + // poll again using the runId +} +``` + +### DetectOutputTranscriptions + +[`DetectOutputTranscriptions`](../docs/api_reference.md#detectoutputtranscriptions) controls the transcription format for audio file deidentification. + +```java +import com.skyflow.enums.DetectOutputTranscriptions; + +DeidentifyFileRequest request = DeidentifyFileRequest.builder() + .file(fileInput) + .outputTranscription(DetectOutputTranscriptions.TRANSCRIPTION) + .build(); +``` + +# Connections + +Skyflow Connections is a gateway service that uses tokenization to securely send and receive data between your systems and first- or third-party services. The [connections](https://github.com/skyflowapi/skyflow-java/tree/main/src/main/java/com/skyflow/vault/connection) module invokes both inbound and/or outbound connections. + +- **Inbound connections**: Act as intermediaries between your client and server, tokenizing sensitive data before it reaches your backend, ensuring downstream services handle only tokenized data. +- **Outbound connections**: Enable secure extraction of data from the vault and transfer it to third-party services via your backend server, such as processing checkout or card issuance flows. + +## ConnectionController + +`ConnectionController` is the class returned by `skyflowClient.connection()` and `skyflowClient.connection(connectionId)`. All connection operations are called on this object. + +```java +// Uses the default (first configured) connection +ConnectionController connection = skyflowClient.connection(); + +// Uses a specific connection by ID +ConnectionController connection = skyflowClient.connection(""); +``` + +**Methods:** + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `invoke(InvokeConnectionRequest)` | [`InvokeConnectionRequest`](../docs/api_reference.md#invokeconnectionrequest) | [`InvokeConnectionResponse`](../docs/api_reference.md#invokeconnectionresponse) | Invoke an inbound or outbound connection | + +## Invoke a connection + +To invoke a connection, use the `invoke` method of the Skyflow client. + +### Construct an invoke connection request + +```java +import com.skyflow.enums.RequestMethod; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.connection.InvokeConnectionResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * This example demonstrates how to invoke an external connection using the Skyflow SDK, along with corresponding InvokeConnectionRequest schema. + * + */ +public class InvokeConnectionSchema { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Define the request body parameters + // These are the values you want to send in the request body + Map requestBody = new HashMap<>(); + requestBody.put("", ""); + requestBody.put("", ""); + + // Step 2: Define the request headers + // Add any required headers that need to be sent with the request + Map requestHeaders = new HashMap<>(); + requestHeaders.put("", ""); + requestHeaders.put("", ""); + + // Step 3: Define the path parameters + // Path parameters are part of the URL and typically used in RESTful APIs + Map pathParams = new HashMap<>(); + pathParams.put("", ""); + pathParams.put("", ""); + + // Step 4: Define the query parameters + // Query parameters are included in the URL after a '?' and are used to filter or modify the response + Map queryParams = new HashMap<>(); + queryParams.put("", ""); + queryParams.put("", ""); + + // Step 5: Build the InvokeConnectionRequest using the provided parameters + InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) // The HTTP method to use for the request (POST in this case) + .requestBody(requestBody) // The body of the request + .requestHeaders(requestHeaders) // The headers to include in the request + .pathParams(pathParams) // The path parameters for the URL + .queryParams(queryParams) // The query parameters to append to the URL + .build(); + + // Step 6: Invoke the connection using the request + // Replace "" with the actual connection ID you are using + InvokeConnectionResponse invokeConnectionResponse = skyflowClient.connection("").invoke(invokeConnectionRequest); + + // Step 7: Print the response from the invoked connection + // This response contains the result of the request sent to the external system + System.out.println(invokeConnectionResponse); + + } catch (SkyflowException e) { + // Step 8: Handle any exceptions that occur during the connection invocation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging + } + } +} +``` + +`method` accepts any [`RequestMethod`](../docs/api_reference.md#requestmethod) value (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`). See [`InvokeConnectionRequest`](../docs/api_reference.md#invokeconnectionrequest) in the API Reference for all builder options. + +**pathParams, queryParams, requestHeader, requestBody** are the JSON objects represented as HashMaps, that will be sent through the connection integration url. + +### An [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/connection/InvokeConnectionExample.java) of invokeConnection + +```java +import com.skyflow.Skyflow; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.Credentials; +import com.skyflow.enums.LogLevel; +import com.skyflow.enums.RequestMethod; +import com.skyflow.errors.SkyflowException; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.connection.InvokeConnectionResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * This example demonstrates how to invoke an external connection using the Skyflow SDK. + * It configures a connection, sets up the request, and sends a POST request to the external service. + * + * 1. Initialize Skyflow client with connection details. + * 2. Define the request body, headers, and method. + * 3. Execute the connection request. + * 4. Print the response from the invoked connection. + */ +public class InvokeConnectionExample { + public static void main(String[] args) { + try { + // Initialize Skyflow client + // Step 1: Set up credentials and connection configuration + // Load credentials from a JSON file (you need to provide the correct path) + Credentials credentials = new Credentials(); + credentials.setPath("/path/to/credentials.json"); + + // Define the connection configuration (URL and credentials) + ConnectionConfig connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId(""); // Replace with actual connection ID + connectionConfig.setConnectionUrl("https://connection.url.com"); // Replace with actual connection URL + connectionConfig.setCredentials(credentials); // Set credentials for the connection + + // Initialize the Skyflow client with the connection configuration + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.DEBUG) // Set log level to DEBUG for detailed logs + .addConnectionConfig(connectionConfig) // Add connection configuration to client + .build(); // Build the Skyflow client instance + + // Step 2: Define the request body and headers + // Map for request body parameters + Map requestBody = new HashMap<>(); + requestBody.put("card_number", "4337-1696-5866-0865"); // Example card number + requestBody.put("ssn", "524-41-4248"); // Example SSN + + // Map for request headers + Map requestHeaders = new HashMap<>(); + requestHeaders.put("Content-Type", "application/json"); // Set content type for the request + + // Step 3: Build the InvokeConnectionRequest with required parameters + // Set HTTP method to POST, include the request body and headers + InvokeConnectionRequest invokeConnectionRequest = InvokeConnectionRequest.builder() + .method(RequestMethod.POST) // HTTP POST method + .requestBody(requestBody) // Add request body parameters + .requestHeaders(requestHeaders) // Add headers + .build(); // Build the request + + // Step 4: Invoke the connection and capture the response + // Replace "" with the actual connection ID + InvokeConnectionResponse invokeConnectionResponse = skyflowClient.connection("").invoke(invokeConnectionRequest); + + // Step 5: Print the response from the connection invocation + System.out.println(invokeConnectionResponse); // Print the response to the console + + } catch (SkyflowException e) { + // Step 6: Handle any exceptions that occur during the connection invocation + System.out.println("Error occurred: "); + e.printStackTrace(); // Print the exception stack trace for debugging + } + } +} +``` + +Sample response: + +```json +{ + "data": { + "card_number": "4337-1696-5866-0865", + "ssn": "524-41-4248" + }, + "metadata": { + "requestId": "4a3453b5-7aa4-4373-98d7-cf102b1f6f97" + } +} +``` + +# Authenticate with bearer tokens + +This section covers methods for generating and managing tokens to authenticate API calls: + +- **Generate a bearer token**: + Enable the creation of bearer tokens using service account credentials. These tokens, valid for 60 minutes, provide secure access to Vault services and management APIs based on the service account's permissions. Use this for general API calls when you only need basic authentication without additional context or role-based restrictions. +- **Generate a bearer token with context**: + Support embedding context values into bearer tokens, enabling dynamic access control and the ability to track end-user identity. These tokens include context claims and allow flexible authorization for Vault services. Use this when policies depend on specific contextual attributes or when tracking end-user identity is required. +- **Generate a scoped bearer token**: + Facilitate the creation of bearer tokens with role-specific access, ensuring permissions are limited to the operations allowed by the designated role. This is particularly useful for service accounts with multiple roles. Use this to enforce fine-grained role-based access control, ensuring tokens only grant permissions for a specific role. +- **Generate signed data tokens**: + Add an extra layer of security by digitally signing data tokens with the service account's private key. These signed tokens can be securely detokenized, provided the necessary bearer token and permissions are available. Use this to add cryptographic protection to sensitive data, enabling secure detokenization with verified integrity and authenticity. + +## Generate a bearer token + +The [Service Account](https://github.com/skyflowapi/skyflow-java/tree/main/src/main/java/com/skyflow/serviceaccount/util) Java module generates service account tokens using a service account credentials file, which is provided when a service account is created. The tokens generated by this module are valid for 60 minutes and can be used to make API calls to the [Data](https://docs.skyflow.com/record/) and [Management](https://docs.skyflow.com/management/) APIs, depending on the permissions assigned to the service account. + +The `BearerToken` utility class generates bearer tokens using a credentials JSON file. Alternatively, you can pass the credentials as a string. + +[Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationExample.java): + +```java +/** + * 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(); + } + } +} +``` + +## Generate bearer tokens with context + +**Context-aware authorization** embeds context values into a bearer token during its generation so you can reference those values in your policies. This enables more flexible access controls, such as helping you track end-user identity when making API calls using service accounts, and facilitates using signed data tokens during detokenization. + +A service account with the `context_id` identifier generates bearer tokens containing context information, represented as a JWT claim in a Skyflow-generated bearer token. Tokens generated from such service accounts include a `context_identifier` claim, are valid for 60 minutes, and can be used to make API calls to the Data and Management APIs, depending on the service account's permissions. + +The `setCtx()` method accepts either a **String** or a **`Map`**: + +**String context** — use when your policy references a single context value: + +```java +BearerToken token = BearerToken.builder() + .setCredentials(new File(filePath)) + .setCtx("user_12345") + .build(); +``` + +**JSON object context** — use when your policy needs multiple context values for conditional data access. Each key in the `Map` maps to a Skyflow CEL policy variable under `request.context.*`: + +```java +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(); +``` + +With the map above, your Skyflow policies can reference `request.context.role`, `request.context.department`, and `request.context.user_id` to make conditional access decisions. + +You can also set context on `Credentials` for automatic token generation: + +```java +// String context +Credentials credentials = new Credentials(); +credentials.setPath("path/to/credentials.json"); +credentials.setContext("user_12345"); + +// Map context +Map ctx = new HashMap<>(); +ctx.put("role", "admin"); +ctx.put("department", "finance"); +credentials.setContext(ctx); +``` + +> **Note:** `getContext()` returns `Object` — callers should use `instanceof` if they need to inspect the type. + +Context map keys must contain only alphanumeric characters and underscores (`[a-zA-Z0-9_]`). Invalid keys will throw a `SkyflowException`. + +[Full example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationWithContextExample.java) + +See Skyflow's [context-aware authorization](https://docs.skyflow.com) and [conditional data access](https://docs.skyflow.com) docs for policy variable syntax like `request.context.*`. + +## Generate scoped bearer tokens + +A service account with multiple roles can generate bearer tokens with access limited to a specific role by specifying the appropriate `roleID`. This can be used to limit access to specific roles for services with multiple responsibilities, such as segregating access for billing and analytics. The generated bearer tokens are valid for 60 minutes and can only execute operations permitted by the permissions associated with the designated role. + +[Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/ScopedTokenGenerationExample.java): + +```java +import com.skyflow.errors.SkyflowException; +import com.skyflow.serviceaccount.util.BearerToken; + +import java.io.File; +import java.util.ArrayList; + +/** + * Example program to generate a Scoped Token using Skyflow's BearerToken utility. + * The token is generated by providing the file path to the credentials.json file + * and specifying roles associated with the token. + */ +public class ScopedTokenGenerationExample { + public static void main(String[] args) { + // Variable to store the generated scoped token + String scopedToken = null; + + // Example: Generate Scoped Token by specifying the credentials.json file path + try { + // Create a list of roles that the generated token will be scoped to + ArrayList roles = new ArrayList<>(); + roles.add("ROLE_ID"); // Add a specific role to the list (e.g., "ROLE_ID") + + // Specify the full file path to the service account's credentials.json file + String filePath = ""; + + // Create a BearerToken object using the credentials file and associated roles + BearerToken bearerToken = BearerToken.builder() + .setCredentials(new File(filePath)) // Set credentials using the credentials.json file + .setRoles(roles) // Set the roles that the token should be scoped to + .build(); // Build the BearerToken object + + // Retrieve the generated scoped token + scopedToken = bearerToken.getBearerToken(); + + // Print the generated scoped token to the console + System.out.println(scopedToken); + } catch (SkyflowException e) { + // Handle exceptions that may occur during token generation + e.printStackTrace(); + } + } +} +``` + +Notes: + +- You can pass either the file path of a service account key credentials file or the service account key credentials as a string to the `setCredentials` method of the `BearerTokenBuilder` class. +- If both a file path and a string are provided, the last method used takes precedence. +- To generate multiple bearer tokens concurrently using threads, refer to the following [example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenGenerationUsingThreadsExample.java). + +## Generate Signed Data Tokens + +Skyflow generates data tokens when sensitive data is inserted into the vault. These data tokens can be digitally signed +with the private key of the service account credentials, which adds an additional layer of protection. Signed tokens can +be detokenized by passing the signed data token and a bearer token generated from service account credentials. The +service account must have appropriate permissions and context to detokenize the signed data tokens. + +The `setCtx()` method on `SignedDataTokensBuilder` also accepts either a **String** or a **`Map`**, using the same format as bearer tokens: + +```java +// String context +SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx("user_12345") + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); + +// JSON object context +Map ctx = new HashMap<>(); +ctx.put("role", "analyst"); +ctx.put("department", "research"); + +SignedDataTokens signedToken = SignedDataTokens.builder() + .setCredentials(new File(filePath)) + .setCtx(ctx) + .setTimeToLive(30) + .setDataTokens(dataTokens) + .build(); +``` + +[Full example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/SignedTokenGenerationExample.java) + +Response: + +```json +[ + { + "dataToken": "5530-4316-0674-5748", + "signedDataToken": "signed_token_eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzLCpZjA" + } +] +``` + +Notes: + +- You can provide either the file path to a service account key credentials file or the service account key credentials as a string to the `setCredentials` method of the `SignedDataTokensBuilder` class. +- If both a file path and a string are passed to the `setCredentials` method, the most recently specified input takes precedence. +- The `time-to-live` (TTL) value should be specified in seconds. +- By default, the TTL value is set to 60 seconds. + +## Bearer token expiry edge case +When you use bearer tokens for authentication and API requests in SDKs, there's the potential for a token to expire after the token is verified as valid but before the actual API call is made, causing the request to fail unexpectedly due to the token's expiration. An error from this edge case would look something like this: + +```txt +message: Authentication failed. Bearer token is expired. Use a valid bearer token. See https://docs.skyflow.com/api-authentication/ +``` + +If you encounter this kind of error, retry the request. During the retry, the SDK detects that the previous bearer token has expired and generates a new one for the current and subsequent requests. + +#### [Example](https://github.com/skyflowapi/skyflow-java/blob/main/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java): + +```java +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.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 DetokenizeExample { + 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 + ArrayList tokenList = new ArrayList<>(); + tokenList.add(""); // First token + tokenList.add(""); // Second token + + // Building a detokenization request with the token list and configuration + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .tokens(tokenList) // Adding tokens to the request + .continueOnError(false) // Stop on error + .redactionType(RedactionType.PLAIN_TEXT) // Redaction type (e.g., PLAIN_TEXT) + .build(); + + // Sending the detokenization request and receiving the response + DetokenizeResponse detokenizeResponse = skyflowClient.vault().detokenize(detokenizeRequest); + + // Printing the detokenized response + System.out.println(detokenizeResponse); + } +} +``` + +# Client Management + +After the `Skyflow` client is built you can add, retrieve, update, or remove vault and connection configurations at runtime — without rebuilding the client. + +## Vault configuration management + +```java +import com.skyflow.config.VaultConfig; + +// Add a new vault at runtime +skyflowClient.addVaultConfig(newVaultConfig); + +// Retrieve the config for a specific vault +VaultConfig config = skyflowClient.getVaultConfig(""); + +// Update an existing vault config (match by vaultId) +skyflowClient.updateVaultConfig(updatedVaultConfig); + +// Remove a vault from the client +skyflowClient.removeVaultConfig(""); +``` + +## Connection configuration management + +```java +import com.skyflow.config.ConnectionConfig; + +// Add a new connection at runtime +skyflowClient.addConnectionConfig(newConnectionConfig); + +// Retrieve the config for a specific connection +ConnectionConfig config = skyflowClient.getConnectionConfig(""); + +// Update an existing connection config (match by connectionId) +skyflowClient.updateConnectionConfig(updatedConnectionConfig); + +// Remove a connection from the client +skyflowClient.removeConnectionConfig(""); +``` + +## Credentials and log level management + +```java +// Replace the Skyflow-level credentials used when vault/connection configs +// do not specify their own credentials +skyflowClient.updateSkyflowCredentials(newCredentials); + +// Update the log level after the client has been built +skyflowClient.updateLogLevel(LogLevel.DEBUG); + +// Read the current log level +LogLevel currentLevel = skyflowClient.getLogLevel(); +``` + +**Client management method reference:** + +| Method | Returns | Description | +|--------|---------|-------------| +| `addVaultConfig(VaultConfig)` | `Skyflow` | Add a vault configuration | +| `getVaultConfig(String vaultId)` | `VaultConfig` | Retrieve a vault configuration by ID | +| `updateVaultConfig(VaultConfig)` | `Skyflow` | Replace a vault configuration (matched by `vaultId`) | +| `removeVaultConfig(String vaultId)` | `Skyflow` | Remove a vault configuration | +| `addConnectionConfig(ConnectionConfig)` | `Skyflow` | Add a connection configuration | +| `getConnectionConfig(String connectionId)` | `ConnectionConfig` | Retrieve a connection configuration by ID | +| `updateConnectionConfig(ConnectionConfig)` | `Skyflow` | Replace a connection configuration | +| `removeConnectionConfig(String connectionId)` | `Skyflow` | Remove a connection configuration | +| `updateSkyflowCredentials(Credentials)` | `Skyflow` | Replace the client-level credentials | +| `updateLogLevel(LogLevel)` | `Skyflow` | Change the log level after initialization | +| `getLogLevel()` | `LogLevel` | Return the current log level | + +All mutating methods return the `Skyflow` instance for chaining and throw `SkyflowException` on validation errors. + +# Error Handling + +The SDK uses `SkyflowException` for all errors — both client-side validation errors and server-side API errors. + +## Catching SkyflowException + +Wrap SDK calls in a `try/catch` block and catch `SkyflowException` to handle Skyflow-specific errors separately from unexpected exceptions: + +```java +import com.skyflow.errors.SkyflowException; + +try { + InsertResponse response = skyflowClient.vault().insert(insertRequest); +} catch (SkyflowException e) { + System.err.println("Skyflow error:"); + System.err.println(" HTTP code : " + e.getHttpCode()); + System.err.println(" Message : " + e.getMessage()); + System.err.println(" Request ID: " + e.getRequestId()); + System.err.println(" Details : " + e.getDetails()); +} catch (Exception e) { + System.err.println("Unexpected error: " + e.getMessage()); +} +``` + +## SkyflowException properties + +| Property | Method | Description | +|---|---|---| +| HTTP status code | `getHttpCode()` | Integer status code (e.g. `400`, `404`, `500`). | +| Message | `getMessage()` | Human-readable description of the error. | +| HTTP status string | `getHttpStatus()` | Status string from the server (e.g. `"BAD_REQUEST"`). | +| gRPC code | `getGrpcCode()` | gRPC status code from the server. | +| Request ID | `getRequestId()` | The `x-request-id` header — useful for support escalations. | +| Details | `getDetails()` | `JsonArray` of additional error context from the server. Empty array for validation errors, `null` if the server response omitted the field. | + +**Validation errors** (missing table name, empty token list, etc.) are thrown before any network call: +- `httpCode` is always `400` +- `requestId` and `grpcCode` are `null` +- `details` is an empty array + +**API errors** are returned by the Skyflow server and have all fields populated from the response body and headers. + +# Logging + +The SDK provides logging with Java's built-in logging library. By default, the SDK's logging level is set to `LogLevel.ERROR`. This can be changed using the `setLogLevel(logLevel)` method, as shown below: + +Currently, the following five log levels are supported: + +- `DEBUG`**:** + When `LogLevel.DEBUG` is passed, logs at all levels will be printed (DEBUG, INFO, WARN, ERROR). +- `INFO`**:** + When `LogLevel.INFO` is passed, INFO logs for every event that occurs during SDK flow execution will be printed, along with WARN and ERROR logs. +- `WARN`**:** + When `LogLevel.WARN` is passed, only WARN and ERROR logs will be printed. +- `ERROR`**:** + When `LogLevel.ERROR` is passed, only ERROR logs will be printed. +- `OFF`**:** + `LogLevel.OFF` can be used to turn off all logging from the Skyflow Java SDK. + +**Note:** The ranking of logging levels is as follows: `DEBUG` \< `INFO` \< `WARN` \< `ERROR` \< `OFF`. + +```java +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 example demonstrates how to configure the Skyflow client with custom log levels + * and authentication credentials (either token, credentials string, or other methods). + * It also shows how to configure a vault connection using specific parameters. + * + * 1. Set up credentials with a Bearer token or credentials string. + * 2. Define the Vault configuration. + * 3. Build the Skyflow client with the chosen configuration and set log level. + * 4. Example of changing the log level from ERROR (default) to INFO. + */ +public class ChangeLogLevel { + public static void main(String[] args) throws SkyflowException { + // Step 1: Set up credentials - either pass token or use credentials string + // In this case, we are using a Bearer token for authentication + Credentials credentials = new Credentials(); + credentials.setToken(""); // Replace with actual Bearer token + + // Step 2: Define the Vault configuration + // Configure the vault with necessary details like vault ID, cluster ID, and environment + VaultConfig config = new VaultConfig(); + config.setVaultId(""); // Replace with actual Vault ID (primary vault) + config.setClusterId(""); // Replace with actual Cluster ID (from vault URL) + config.setEnv(Env.PROD); // Set the environment (default is PROD) + config.setCredentials(credentials); // Set credentials for the vault (either token or credentials) + + // Step 3: Define additional Skyflow credentials (optional, if needed for credentials string) + // Create a JSON object to hold your Skyflow credentials + JsonObject credentialsObject = new JsonObject(); + credentialsObject.addProperty("clientId", ""); // Replace with your client ID + credentialsObject.addProperty("clientName", ""); // Replace with your client name + credentialsObject.addProperty("tokenUri", ""); // Replace with your token URI + credentialsObject.addProperty("keyId", ""); // Replace with your key ID + credentialsObject.addProperty("privateKey", ""); // Replace with your private key + + // Convert the credentials object to a string format to be used for generating a Bearer Token + Credentials skyflowCredentials = new Credentials(); + skyflowCredentials.setCredentialsString(credentialsObject.toString()); // Set credentials string + + // Step 4: Build the Skyflow client with the chosen configuration and log level + Skyflow skyflowClient = Skyflow.builder() + .addVaultConfig(config) // Add the Vault configuration + .addSkyflowCredentials(skyflowCredentials) // Use Skyflow credentials if no token is passed + .setLogLevel(LogLevel.INFO) // Set log level to INFO (default is ERROR) + .build(); // Build the Skyflow client + + // Now, the Skyflow client is ready to use with the specified log level and credentials + System.out.println("Skyflow client has been successfully configured with log level: INFO."); + } +} +``` + +# Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at **security@skyflow.com**. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. diff --git a/skyvault/api-report/skyflow-java.baseline.jar b/skyvault/api-report/skyflow-java.baseline.jar new file mode 100644 index 00000000..81085660 Binary files /dev/null and b/skyvault/api-report/skyflow-java.baseline.jar differ diff --git a/skyvault/pom.xml b/skyvault/pom.xml new file mode 100644 index 00000000..618c7e9d --- /dev/null +++ b/skyvault/pom.xml @@ -0,0 +1,216 @@ + + + 4.0.0 + + com.skyflow + skyflow + 1.0.0 + ../pom.xml + + + skyflow-java + 2.1.1 + com.skyflow + + + false + 8 + 8 + UTF-8 + ${project.version} + + + + + com.skyflow + common + 1.0.0 + + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + com.skyflow:common + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + shade-for-japicmp + package + + shade + + + true + with-common + + + com.skyflow:common + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + 0.26.0 + + + + + ${project.basedir}/api-report/skyflow-java.baseline.jar + + + + + ${project.build.directory}/${project.build.finalName}-with-common.jar + + + + protected + true + false + true + true + + + com.skyflow.Skyflow + com.skyflow.config + com.skyflow.enums + com.skyflow.errors + com.skyflow.serviceaccount.util + com.skyflow.vault.audit + com.skyflow.vault.bin + com.skyflow.vault.connection + com.skyflow.vault.controller + com.skyflow.vault.data + com.skyflow.vault.detect + com.skyflow.vault.tokens + + false + false + + + + + default-cli + verify + + cmp + + + + + + + + + jfrog + + + central + prekarilabs.jfrog.io-releases + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + snapshots + prekarilabs.jfrog.io-snapshots + https://prekarilabs.jfrog.io/artifactory/skyflow-java + + + + + maven-central + + + central + https://central.sonatype.com/api/v1/publisher/upload + + + central-snapshots + https://central.sonatype.com/api/v1/publisher/upload + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + central + true + + false + + + + + + + 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/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java b/skyvault/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 skyvault/samples/src/main/java/com/example/serviceaccount/BearerTokenExpiryExample.java 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/src/main/java/com/skyflow/ConnectionClient.java b/skyvault/src/main/java/com/skyflow/ConnectionClient.java similarity index 100% rename from src/main/java/com/skyflow/ConnectionClient.java rename to skyvault/src/main/java/com/skyflow/ConnectionClient.java diff --git a/src/main/java/com/skyflow/Skyflow.java b/skyvault/src/main/java/com/skyflow/Skyflow.java similarity index 52% rename from src/main/java/com/skyflow/Skyflow.java rename to skyvault/src/main/java/com/skyflow/Skyflow.java index eba8d6fd..940f5008 100644 --- a/src/main/java/com/skyflow/Skyflow.java +++ b/skyvault/src/main/java/com/skyflow/Skyflow.java @@ -3,7 +3,6 @@ import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; @@ -19,35 +18,58 @@ import java.util.LinkedHashMap; -public final class Skyflow { +public final class Skyflow extends BaseSkyflow { private final SkyflowClientBuilder builder; private Skyflow(SkyflowClientBuilder builder) { + super(builder); this.builder = builder; - LogUtil.printInfoLog(InfoLogs.CLIENT_INITIALIZED.getLog()); + } + + @Override + protected Skyflow self() { + return this; } public static SkyflowClientBuilder builder() { return new SkyflowClientBuilder(); } + // ── Covariant overrides ─────────────────────────────────────────────────── + // BaseSkyflow declares these as `Self` / `V`, which erase to BaseSkyflow and BaseVaultConfig. + // Source callers are unaffected because javac resolves the type parameters, but a consumer + // JAR compiled against skyflow-java 2.1.1 references the concrete descriptors and would fail + // with NoSuchMethodError against an erased-only surface. Re-declaring them keeps the published + // 2.x binary contract intact; each one just delegates. + + @Override public Skyflow addVaultConfig(VaultConfig vaultConfig) throws SkyflowException { - this.builder.addVaultConfig(vaultConfig); - return this; + return super.addVaultConfig(vaultConfig); } + @Override public VaultConfig getVaultConfig(String vaultId) { - return this.builder.vaultConfigMap.get(vaultId); + return super.getVaultConfig(vaultId); } + @Override public Skyflow updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { - this.builder.updateVaultConfig(vaultConfig); - return this; + return super.updateVaultConfig(vaultConfig); } + @Override public Skyflow removeVaultConfig(String vaultId) throws SkyflowException { - this.builder.removeVaultConfig(vaultId); - return this; + return super.removeVaultConfig(vaultId); + } + + @Override + public Skyflow updateSkyflowCredentials(Credentials credentials) throws SkyflowException { + return super.updateSkyflowCredentials(credentials); + } + + @Override + public Skyflow setLogLevel(LogLevel logLevel) { + return super.setLogLevel(logLevel); } public Skyflow addConnectionConfig(ConnectionConfig connectionConfig) throws SkyflowException { @@ -69,16 +91,6 @@ public Skyflow removeConnectionConfig(String connectionId) throws SkyflowExcepti return this; } - public Skyflow updateSkyflowCredentials(Credentials credentials) throws SkyflowException { - this.builder.addSkyflowCredentials(credentials); - return this; - } - - public Skyflow setLogLevel(LogLevel logLevel) { - this.builder.setLogLevel(logLevel); - return this; - } - /** @deprecated Use {@link #setLogLevel(LogLevel)} instead. */ @Deprecated(since = "2.1", forRemoval = true) public Skyflow updateLogLevel(LogLevel logLevel) { @@ -86,132 +98,75 @@ public Skyflow updateLogLevel(LogLevel logLevel) { return setLogLevel(logLevel); } - public LogLevel getLogLevel() { - return this.builder.logLevel; - } - public VaultController vault() throws SkyflowException { - Object[] array = this.builder.vaultClientsMap.keySet().toArray(); - if (array.length < 1) { - LogUtil.printErrorLog(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); - } - String vaultId = (String) array[0]; - return this.vault(vaultId); + return resolveOrThrow(this.builder.vaultClientsMap, null, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); } public VaultController vault(String vaultId) throws SkyflowException { - VaultController controller = this.builder.vaultClientsMap.get(vaultId); - if (controller == null) { - LogUtil.printErrorLog(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); - } - return controller; + return resolveOrThrow(this.builder.vaultClientsMap, vaultId, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); } - public ConnectionController connection() throws SkyflowException { - Object[] array = this.builder.connectionsMap.keySet().toArray(); - if (array.length < 1) { - LogUtil.printErrorLog(ErrorLogs.CONNECTION_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.ConnectionIdNotInConfigList.getMessage()); - } - String connectionId = (String) array[0]; - return this.connection(connectionId); + return resolveOrThrow(this.builder.connectionsMap, null, ErrorLogs.CONNECTION_CONFIG_DOES_NOT_EXIST, ErrorMessage.ConnectionIdNotInConfigList); } public ConnectionController connection(String connectionId) throws SkyflowException { - ConnectionController controller = this.builder.connectionsMap.get(connectionId); - if (controller == null) { - LogUtil.printErrorLog(ErrorLogs.CONNECTION_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.ConnectionIdNotInConfigList.getMessage()); - } - return controller; + return resolveOrThrow(this.builder.connectionsMap, connectionId, ErrorLogs.CONNECTION_CONFIG_DOES_NOT_EXIST, ErrorMessage.ConnectionIdNotInConfigList); } public DetectController detect() throws SkyflowException { - Object[] array = this.builder.detectClientsMap.keySet().toArray(); - if (array.length < 1) { - LogUtil.printErrorLog(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); - } - String detectId = (String) array[0]; - return this.detect(detectId); + return resolveOrThrow(this.builder.detectClientsMap, null, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); } public DetectController detect(String vaultId) throws SkyflowException { - DetectController controller = this.builder.detectClientsMap.get(vaultId); - if (controller == null) { - LogUtil.printErrorLog(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); - } - return controller; + return resolveOrThrow(this.builder.detectClientsMap, vaultId, ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST, ErrorMessage.VaultIdNotInConfigList); } - public static final class SkyflowClientBuilder { - private final LinkedHashMap connectionsMap; - private final LinkedHashMap vaultClientsMap; - private final LinkedHashMap detectClientsMap; - private final LinkedHashMap vaultConfigMap; - private final LinkedHashMap connectionConfigMap; - private Credentials skyflowCredentials; - private LogLevel logLevel; - - public SkyflowClientBuilder() { - this.vaultClientsMap = new LinkedHashMap<>(); - this.detectClientsMap = new LinkedHashMap<>(); - this.vaultConfigMap = new LinkedHashMap<>(); - this.connectionsMap = new LinkedHashMap<>(); - this.connectionConfigMap = new LinkedHashMap<>(); - this.skyflowCredentials = null; - this.logLevel = LogLevel.ERROR; - } + public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder { + private final LinkedHashMap vaultClientsMap = new LinkedHashMap<>(); + private final LinkedHashMap connectionsMap = new LinkedHashMap<>(); + private final LinkedHashMap detectClientsMap = new LinkedHashMap<>(); + private final LinkedHashMap connectionConfigMap = new LinkedHashMap<>(); - public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws SkyflowException { - LogUtil.printInfoLog(InfoLogs.VALIDATING_VAULT_CONFIG.getLog()); + @Override + protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { Validations.validateVaultConfig(vaultConfig); - if (this.vaultClientsMap.containsKey(vaultConfig.getVaultId())) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.VAULT_CONFIG_EXISTS.getLog(), vaultConfig.getVaultId() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - ErrorMessage.VaultIdAlreadyInConfigList.getMessage()); - } else { - this.vaultConfigMap.put(vaultConfig.getVaultId(), vaultConfig); - this.vaultClientsMap.put(vaultConfig.getVaultId(), new VaultController(vaultConfig, this.skyflowCredentials)); - this.detectClientsMap.put(vaultConfig.getVaultId(), new DetectController(vaultConfig, this.skyflowCredentials)); - LogUtil.printInfoLog(Utils.parameterizedString( - InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); - LogUtil.printInfoLog(Utils.parameterizedString( - InfoLogs.DETECT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); - } - return this; } - public SkyflowClientBuilder updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { - LogUtil.printInfoLog(InfoLogs.VALIDATING_VAULT_CONFIG.getLog()); - Validations.validateVaultConfig(vaultConfig); - if (this.vaultClientsMap.containsKey(vaultConfig.getVaultId())) { - VaultConfig updatedConfig = findAndUpdateVaultConfig(vaultConfig); - this.vaultClientsMap.get(updatedConfig.getVaultId()).updateVaultConfig(); - } else { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog(), vaultConfig.getVaultId() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); - } - return this; + @Override + protected void onVaultConfigAdded(VaultConfig vaultConfig) throws SkyflowException { + this.vaultClientsMap.put(vaultConfig.getVaultId(), new VaultController(vaultConfig, this.skyflowCredentials)); + this.detectClientsMap.put(vaultConfig.getVaultId(), new DetectController(vaultConfig, this.skyflowCredentials)); + LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); + LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.DETECT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId())); } - public SkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { - if (this.vaultClientsMap.containsKey(vaultId)) { - this.vaultClientsMap.remove(vaultId); - this.vaultConfigMap.remove(vaultId); - } else { - LogUtil.printErrorLog(Utils.parameterizedString(ErrorLogs.VAULT_CONFIG_DOES_NOT_EXIST.getLog(), vaultId)); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.VaultIdNotInConfigList.getMessage()); + @Override + protected void onVaultConfigUpdated(VaultConfig updatedConfig) throws SkyflowException { + this.vaultClientsMap.get(updatedConfig.getVaultId()).updateVaultConfig(updatedConfig); + } + + @Override + protected void onVaultConfigRemoved(String vaultId) throws SkyflowException { + this.vaultClientsMap.remove(vaultId); + } + + @Override + protected boolean hasVaultClient(String vaultId) { + return this.vaultClientsMap.containsKey(vaultId); + } + + @Override + protected void onCredentialsUpdated(Credentials credentials) throws SkyflowException { + for (VaultController vault : this.vaultClientsMap.values()) { + vault.setCommonCredentials(credentials); + } + for (DetectController detect : this.detectClientsMap.values()) { + detect.setCommonCredentials(credentials); + } + for (ConnectionController connection : this.connectionsMap.values()) { + connection.setCommonCredentials(credentials); } - return this; } public SkyflowClientBuilder addConnectionConfig(ConnectionConfig connectionConfig) throws SkyflowException { @@ -263,27 +218,33 @@ public SkyflowClientBuilder removeConnectionConfig(String connectionId) throws S return this; } + @Override + public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + super.addVaultConfig(vaultConfig); + return this; + } + + @Override + public SkyflowClientBuilder updateVaultConfig(VaultConfig vaultConfig) throws SkyflowException { + super.updateVaultConfig(vaultConfig); + return this; + } + + @Override + public SkyflowClientBuilder removeVaultConfig(String vaultId) throws SkyflowException { + super.removeVaultConfig(vaultId); + return this; + } + + @Override public SkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throws SkyflowException { - Validations.validateCredentials(credentials); - this.skyflowCredentials = credentials; - for (VaultController vault : this.vaultClientsMap.values()) { - vault.setCommonCredentials(this.skyflowCredentials); - } - for (DetectController detect : this.detectClientsMap.values()) { - detect.setCommonCredentials(this.skyflowCredentials); - } - for (ConnectionController connection : this.connectionsMap.values()) { - connection.setCommonCredentials(this.skyflowCredentials); - } + super.addSkyflowCredentials(credentials); return this; } + @Override public SkyflowClientBuilder setLogLevel(LogLevel logLevel) { - this.logLevel = logLevel == null ? LogLevel.ERROR : logLevel; - LogUtil.setupLogger(this.logLevel); - LogUtil.printInfoLog(Utils.parameterizedString( - InfoLogs.CURRENT_LOG_LEVEL.getLog(), String.valueOf(logLevel) - )); + super.setLogLevel(logLevel); return this; } @@ -291,17 +252,6 @@ public Skyflow build() { return new Skyflow(this); } - private VaultConfig findAndUpdateVaultConfig(VaultConfig vaultConfig) { - VaultConfig previousConfig = this.vaultConfigMap.get(vaultConfig.getVaultId()); - Env env = vaultConfig.getEnv() != null ? vaultConfig.getEnv() : previousConfig.getEnv(); - String clusterId = vaultConfig.getClusterId() != null ? vaultConfig.getClusterId() : previousConfig.getClusterId(); - Credentials credentials = vaultConfig.getCredentials() != null ? vaultConfig.getCredentials() : previousConfig.getCredentials(); - previousConfig.setEnv(env); - previousConfig.setClusterId(clusterId); - previousConfig.setCredentials(credentials); - return previousConfig; - } - private ConnectionConfig findAndUpdateConnectionConfig(ConnectionConfig connectionConfig) { ConnectionConfig previousConfig = this.connectionConfigMap.get(connectionConfig.getConnectionId()); String connectionURL = connectionConfig.getConnectionUrl() != null ? connectionConfig.getConnectionUrl() : previousConfig.getConnectionUrl(); @@ -311,4 +261,4 @@ private ConnectionConfig findAndUpdateConnectionConfig(ConnectionConfig connecti return previousConfig; } } -} +} \ No newline at end of file diff --git a/src/main/java/com/skyflow/VaultClient.java b/skyvault/src/main/java/com/skyflow/VaultClient.java similarity index 91% rename from src/main/java/com/skyflow/VaultClient.java rename to skyvault/src/main/java/com/skyflow/VaultClient.java index 1d5e5d74..6a0a3134 100644 --- a/src/main/java/com/skyflow/VaultClient.java +++ b/skyvault/src/main/java/com/skyflow/VaultClient.java @@ -26,12 +26,7 @@ import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; import com.skyflow.generated.rest.types.Transformations; import com.skyflow.generated.rest.types.*; -import com.skyflow.logs.InfoLogs; -import com.skyflow.serviceaccount.util.Token; -import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; -import com.skyflow.utils.logger.LogUtil; -import com.skyflow.utils.validations.Validations; import com.skyflow.vault.data.FileUploadRequest; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.UpdateRequest; @@ -41,35 +36,30 @@ import com.skyflow.vault.tokens.DetokenizeData; import com.skyflow.vault.tokens.DetokenizeRequest; import com.skyflow.vault.tokens.TokenizeRequest; -import io.github.cdimascio.dotenv.Dotenv; -import io.github.cdimascio.dotenv.DotenvException; -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; -import okhttp3.Request; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.*; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -public class VaultClient { - private final VaultConfig vaultConfig; +public class VaultClient extends BaseVaultClient { + + /** + * Restores the concrete descriptor from skyflow-java 2.1.1. BaseVaultClient declares this as + * {@code V getVaultConfig()}, which erases to BaseVaultConfig; VaultController and + * DetectController are compiled against the concrete form. + */ + @Override + protected VaultConfig getVaultConfig() { + return super.getVaultConfig(); + } private final ApiClientBuilder apiClientBuilder; private ApiClient apiClient; - private OkHttpClient sharedHttpClient; - private String currentVaultURL; - private Credentials commonCredentials; - private Credentials finalCredentials; - private String token; - private String apiKey; protected VaultClient(VaultConfig vaultConfig, Credentials credentials) { - super(); - this.vaultConfig = vaultConfig; - this.commonCredentials = credentials; + super(vaultConfig, credentials); this.apiClientBuilder = new ApiClientBuilder(); this.apiClient = null; updateVaultURL(); @@ -95,18 +85,19 @@ protected QueryClient getQueryApi() { return this.apiClient.query(); } - protected VaultConfig getVaultConfig() { - return vaultConfig; - } - protected void setCommonCredentials(Credentials commonCredentials) throws SkyflowException { this.commonCredentials = commonCredentials; - prioritiseCredentials(); + super.prioritiseCredentials(this.vaultConfig.getCredentials()); } protected void updateVaultConfig() throws SkyflowException { updateVaultURL(); - prioritiseCredentials(); + super.prioritiseCredentials(this.vaultConfig.getCredentials()); + } + + protected void updateVaultConfig(VaultConfig newConfig) throws SkyflowException { + this.vaultConfig = newConfig; + updateVaultConfig(); } protected V1DetokenizePayload getDetokenizePayload(DetokenizeRequest request) { @@ -233,19 +224,7 @@ protected File getFileForFileUpload(FileUploadRequest fileUploadRequest) throws } protected void setBearerToken() throws SkyflowException { - prioritiseCredentials(); - Validations.validateCredentials(this.finalCredentials); - if (this.finalCredentials.getApiKey() != null) { - LogUtil.printInfoLog(InfoLogs.REUSE_API_KEY.getLog()); - token = this.finalCredentials.getApiKey(); - } else if (token == null || token.trim().isEmpty()) { - token = Utils.generateBearerToken(this.finalCredentials); - } else if (Token.isExpired(token)) { - LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_EXPIRED.getLog()); - token = Utils.generateBearerToken(this.finalCredentials); - } else { - LogUtil.printInfoLog(InfoLogs.REUSE_BEARER_TOKEN.getLog()); - } + super.setBearerToken(this.vaultConfig.getCredentials()); if (apiClient == null) { updateExecutorInHTTP(); this.apiClient = this.apiClientBuilder.build(); @@ -838,48 +817,9 @@ private void updateVaultURL() { private void updateExecutorInHTTP() { if (sharedHttpClient == null) { - sharedHttpClient = new OkHttpClient.Builder() - .connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES)) - .addInterceptor(chain -> { - Request requestWithAuth = chain.request().newBuilder() - .header("Authorization", "Bearer " + this.token) - .build(); - return chain.proceed(requestWithAuth); - }) - .build(); + sharedHttpClient = buildSharedHttpClient(() -> this.token); apiClientBuilder.httpClient(sharedHttpClient); } } - private void prioritiseCredentials() throws SkyflowException { - try { - Credentials original = this.finalCredentials; - if (this.vaultConfig.getCredentials() != null) { - this.finalCredentials = this.vaultConfig.getCredentials(); - } else if (this.commonCredentials != null) { - this.finalCredentials = this.commonCredentials; - } else { - Dotenv dotenv = Dotenv.load(); - String sysCredentials = dotenv.get(Constants.ENV_CREDENTIALS_KEY_NAME); - if (sysCredentials == null) { - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - ErrorMessage.EmptyCredentials.getMessage()); - } else { - this.finalCredentials = new Credentials(); - this.finalCredentials.setCredentialsString(sysCredentials); - } - } - if (original != null && !original.equals(this.finalCredentials)) { - token = null; - apiKey = null; - } - } catch (DotenvException e) { - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - ErrorMessage.EmptyCredentials.getMessage()); - } catch (SkyflowException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/com/skyflow/config/ConnectionConfig.java b/skyvault/src/main/java/com/skyflow/config/ConnectionConfig.java similarity index 100% rename from src/main/java/com/skyflow/config/ConnectionConfig.java rename to skyvault/src/main/java/com/skyflow/config/ConnectionConfig.java diff --git a/src/main/java/com/skyflow/config/ManagementConfig.java b/skyvault/src/main/java/com/skyflow/config/ManagementConfig.java similarity index 100% rename from src/main/java/com/skyflow/config/ManagementConfig.java rename to skyvault/src/main/java/com/skyflow/config/ManagementConfig.java diff --git a/skyvault/src/main/java/com/skyflow/config/VaultConfig.java b/skyvault/src/main/java/com/skyflow/config/VaultConfig.java new file mode 100644 index 00000000..2abd6a27 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/config/VaultConfig.java @@ -0,0 +1,9 @@ +package com.skyflow.config; + +public class VaultConfig extends BaseVaultConfig { + + public VaultConfig() { + super(); + } + +} diff --git a/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java b/skyvault/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java similarity index 100% rename from src/main/java/com/skyflow/enums/DeidentifyFileStatus.java rename to skyvault/src/main/java/com/skyflow/enums/DeidentifyFileStatus.java diff --git a/src/main/java/com/skyflow/enums/DetectEntities.java b/skyvault/src/main/java/com/skyflow/enums/DetectEntities.java similarity index 100% rename from src/main/java/com/skyflow/enums/DetectEntities.java rename to skyvault/src/main/java/com/skyflow/enums/DetectEntities.java diff --git a/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java b/skyvault/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java similarity index 100% rename from src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java rename to skyvault/src/main/java/com/skyflow/enums/DetectOutputTranscriptions.java diff --git a/skyvault/src/main/java/com/skyflow/enums/Env.java b/skyvault/src/main/java/com/skyflow/enums/Env.java new file mode 100644 index 00000000..3d1466e9 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/enums/Env.java @@ -0,0 +1,8 @@ +package com.skyflow.enums; + +public enum Env { + DEV, + STAGE, + SANDBOX, + PROD +} diff --git a/src/main/java/com/skyflow/enums/InterfaceName.java b/skyvault/src/main/java/com/skyflow/enums/InterfaceName.java similarity index 100% rename from src/main/java/com/skyflow/enums/InterfaceName.java rename to skyvault/src/main/java/com/skyflow/enums/InterfaceName.java diff --git a/skyvault/src/main/java/com/skyflow/enums/LogLevel.java b/skyvault/src/main/java/com/skyflow/enums/LogLevel.java new file mode 100644 index 00000000..a605b520 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/enums/LogLevel.java @@ -0,0 +1,9 @@ +package com.skyflow.enums; + +public enum LogLevel { + OFF, + ERROR, + WARN, + INFO, + DEBUG +} diff --git a/src/main/java/com/skyflow/enums/MaskingMethod.java b/skyvault/src/main/java/com/skyflow/enums/MaskingMethod.java similarity index 100% rename from src/main/java/com/skyflow/enums/MaskingMethod.java rename to skyvault/src/main/java/com/skyflow/enums/MaskingMethod.java diff --git a/src/main/java/com/skyflow/enums/RedactionType.java b/skyvault/src/main/java/com/skyflow/enums/RedactionType.java similarity index 100% rename from src/main/java/com/skyflow/enums/RedactionType.java rename to skyvault/src/main/java/com/skyflow/enums/RedactionType.java diff --git a/src/main/java/com/skyflow/enums/RequestMethod.java b/skyvault/src/main/java/com/skyflow/enums/RequestMethod.java similarity index 100% rename from src/main/java/com/skyflow/enums/RequestMethod.java rename to skyvault/src/main/java/com/skyflow/enums/RequestMethod.java diff --git a/src/main/java/com/skyflow/enums/TokenMode.java b/skyvault/src/main/java/com/skyflow/enums/TokenMode.java similarity index 100% rename from src/main/java/com/skyflow/enums/TokenMode.java rename to skyvault/src/main/java/com/skyflow/enums/TokenMode.java diff --git a/src/main/java/com/skyflow/enums/TokenType.java b/skyvault/src/main/java/com/skyflow/enums/TokenType.java similarity index 100% rename from src/main/java/com/skyflow/enums/TokenType.java rename to skyvault/src/main/java/com/skyflow/enums/TokenType.java diff --git a/src/main/java/com/skyflow/errors/ErrorMessage.java b/skyvault/src/main/java/com/skyflow/errors/ErrorMessage.java similarity index 100% rename from src/main/java/com/skyflow/errors/ErrorMessage.java rename to skyvault/src/main/java/com/skyflow/errors/ErrorMessage.java diff --git a/src/main/java/com/skyflow/generated/rest/ApiClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/ApiClient.java similarity index 88% rename from src/main/java/com/skyflow/generated/rest/ApiClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/ApiClient.java index 1eb1e429..ab81f443 100644 --- a/src/main/java/com/skyflow/generated/rest/ApiClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/ApiClient.java @@ -6,7 +6,6 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.Suppliers; import com.skyflow.generated.rest.resources.audit.AuditClient; -import com.skyflow.generated.rest.resources.authentication.AuthenticationClient; import com.skyflow.generated.rest.resources.binlookup.BinLookupClient; import com.skyflow.generated.rest.resources.files.FilesClient; import com.skyflow.generated.rest.resources.guardrails.GuardrailsClient; @@ -14,6 +13,7 @@ import com.skyflow.generated.rest.resources.records.RecordsClient; import com.skyflow.generated.rest.resources.strings.StringsClient; import com.skyflow.generated.rest.resources.tokens.TokensClient; + import java.util.function.Supplier; public class ApiClient { @@ -29,8 +29,6 @@ public class ApiClient { protected final Supplier queryClient; - protected final Supplier authenticationClient; - protected final Supplier filesClient; protected final Supplier stringsClient; @@ -44,7 +42,6 @@ public ApiClient(ClientOptions clientOptions) { this.recordsClient = Suppliers.memoize(() -> new RecordsClient(clientOptions)); this.tokensClient = Suppliers.memoize(() -> new TokensClient(clientOptions)); this.queryClient = Suppliers.memoize(() -> new QueryClient(clientOptions)); - this.authenticationClient = Suppliers.memoize(() -> new AuthenticationClient(clientOptions)); this.filesClient = Suppliers.memoize(() -> new FilesClient(clientOptions)); this.stringsClient = Suppliers.memoize(() -> new StringsClient(clientOptions)); this.guardrailsClient = Suppliers.memoize(() -> new GuardrailsClient(clientOptions)); @@ -70,10 +67,6 @@ public QueryClient query() { return this.queryClient.get(); } - public AuthenticationClient authentication() { - return this.authenticationClient.get(); - } - public FilesClient files() { return this.filesClient.get(); } diff --git a/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 src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java rename to skyvault/src/main/java/com/skyflow/generated/rest/ApiClientBuilder.java diff --git a/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java b/skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java similarity index 88% rename from src/main/java/com/skyflow/generated/rest/AsyncApiClient.java rename to skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java index 405d0208..dc57df06 100644 --- a/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClient.java @@ -6,7 +6,6 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.Suppliers; import com.skyflow.generated.rest.resources.audit.AsyncAuditClient; -import com.skyflow.generated.rest.resources.authentication.AsyncAuthenticationClient; import com.skyflow.generated.rest.resources.binlookup.AsyncBinLookupClient; import com.skyflow.generated.rest.resources.files.AsyncFilesClient; import com.skyflow.generated.rest.resources.guardrails.AsyncGuardrailsClient; @@ -14,6 +13,7 @@ import com.skyflow.generated.rest.resources.records.AsyncRecordsClient; import com.skyflow.generated.rest.resources.strings.AsyncStringsClient; import com.skyflow.generated.rest.resources.tokens.AsyncTokensClient; + import java.util.function.Supplier; public class AsyncApiClient { @@ -29,8 +29,6 @@ public class AsyncApiClient { protected final Supplier queryClient; - protected final Supplier authenticationClient; - protected final Supplier filesClient; protected final Supplier stringsClient; @@ -44,7 +42,6 @@ public AsyncApiClient(ClientOptions clientOptions) { this.recordsClient = Suppliers.memoize(() -> new AsyncRecordsClient(clientOptions)); this.tokensClient = Suppliers.memoize(() -> new AsyncTokensClient(clientOptions)); this.queryClient = Suppliers.memoize(() -> new AsyncQueryClient(clientOptions)); - this.authenticationClient = Suppliers.memoize(() -> new AsyncAuthenticationClient(clientOptions)); this.filesClient = Suppliers.memoize(() -> new AsyncFilesClient(clientOptions)); this.stringsClient = Suppliers.memoize(() -> new AsyncStringsClient(clientOptions)); this.guardrailsClient = Suppliers.memoize(() -> new AsyncGuardrailsClient(clientOptions)); @@ -70,10 +67,6 @@ public AsyncQueryClient query() { return this.queryClient.get(); } - public AsyncAuthenticationClient authentication() { - return this.authenticationClient.get(); - } - public AsyncFilesClient files() { return this.filesClient.get(); } diff --git a/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 src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java rename to skyvault/src/main/java/com/skyflow/generated/rest/AsyncApiClientBuilder.java diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java new file mode 100644 index 00000000..be5247eb --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientApiException.java @@ -0,0 +1,74 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.Response; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This exception type will be thrown for any non-2XX API responses. + */ +public class ApiClientApiException extends ApiClientException { + /** + * The error code of the response that triggered the exception. + */ + private final int statusCode; + + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + private final Map> headers; + + public ApiClientApiException(String message, int statusCode, Object body) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + } + + public ApiClientApiException(String message, int statusCode, Object body, Response rawResponse) { + super(message); + this.statusCode = statusCode; + this.body = body; + this.headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + this.headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + } + + /** + * @return the statusCode + */ + public int statusCode() { + return this.statusCode; + } + + /** + * @return the body + */ + public Object body() { + return this.body; + } + + /** + * @return the headers + */ + public Map> headers() { + return this.headers; + } + + @Override + public String toString() { + return "ApiClientApiException{" + "message: " + getMessage() + ", statusCode: " + statusCode + ", body: " + body + + "}"; + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java new file mode 100644 index 00000000..7987eba6 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientException.java @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +/** + * This class serves as the base exception for all errors in the SDK. + */ +public class ApiClientException extends RuntimeException { + public ApiClientException(String message) { + super(message); + } + + public ApiClientException(String message, Exception e) { + super(message, e); + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java new file mode 100644 index 00000000..c743352c --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ApiClientHttpResponse.java @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.Response; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class ApiClientHttpResponse { + + private final T body; + + private final Map> headers; + + public ApiClientHttpResponse(T body, Response rawResponse) { + this.body = body; + + Map> headers = new HashMap<>(); + rawResponse.headers().forEach(header -> { + String key = header.component1(); + String value = header.component2(); + headers.computeIfAbsent(key, _str -> new ArrayList<>()).add(value); + }); + this.headers = headers; + } + + public T body() { + return this.body; + } + + public Map> headers() { + return headers; + } +} diff --git a/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java similarity index 99% rename from src/main/java/com/skyflow/generated/rest/core/ClientOptions.java rename to skyvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java index ddb0e107..da41fb25 100644 --- a/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ClientOptions.java @@ -3,12 +3,13 @@ */ package com.skyflow.generated.rest.core; +import okhttp3.OkHttpClient; + import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import okhttp3.OkHttpClient; public final class ClientOptions { private final Environment environment; diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java new file mode 100644 index 00000000..a0a6d7c4 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/DateTimeDeserializer.java @@ -0,0 +1,56 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; + +/** + * Custom deserializer that handles converting ISO8601 dates into {@link OffsetDateTime} objects. + */ +class DateTimeDeserializer extends JsonDeserializer { + private static final SimpleModule MODULE; + + static { + MODULE = new SimpleModule().addDeserializer(OffsetDateTime.class, new DateTimeDeserializer()); + } + + /** + * Gets a module wrapping this deserializer as an adapter for the Jackson ObjectMapper. + * + * @return A {@link SimpleModule} to be plugged onto Jackson ObjectMapper. + */ + public static SimpleModule getModule() { + return MODULE; + } + + @Override + public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonToken token = parser.currentToken(); + if (token == JsonToken.VALUE_NUMBER_INT) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(parser.getValueAsLong()), ZoneOffset.UTC); + } else { + TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest( + parser.getValueAsString(), OffsetDateTime::from, LocalDateTime::from); + + if (temporal.query(TemporalQueries.offset()) == null) { + return LocalDateTime.from(temporal).atOffset(ZoneOffset.UTC); + } else { + return OffsetDateTime.from(temporal); + } + } + } +} diff --git a/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 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/skyvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java new file mode 100644 index 00000000..2131b0a4 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/FileStream.java @@ -0,0 +1,61 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import org.jetbrains.annotations.Nullable; + +import java.io.InputStream; +import java.util.Objects; + +/** + * Represents a file stream with associated metadata for file uploads. + */ +public class FileStream { + private final InputStream inputStream; + private final String fileName; + private final MediaType contentType; + + /** + * Constructs a FileStream with the given input stream and optional metadata. + * + * @param inputStream The input stream of the file content. Must not be null. + * @param fileName The name of the file, or null if unknown. + * @param contentType The MIME type of the file content, or null if unknown. + * @throws NullPointerException if inputStream is null + */ + public FileStream(InputStream inputStream, @Nullable String fileName, @Nullable MediaType contentType) { + this.inputStream = Objects.requireNonNull(inputStream, "Input stream cannot be null"); + this.fileName = fileName; + this.contentType = contentType; + } + + public FileStream(InputStream inputStream) { + this(inputStream, null, null); + } + + public InputStream getInputStream() { + return inputStream; + } + + @Nullable + public String getFileName() { + return fileName; + } + + @Nullable + public MediaType getContentType() { + return contentType; + } + + /** + * Creates a RequestBody suitable for use with OkHttp client. + * + * @return A RequestBody instance representing this file stream. + */ + public RequestBody toRequestBody() { + return new InputStreamRequestBody(contentType, inputStream); + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java new file mode 100644 index 00000000..55c3c971 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/InputStreamRequestBody.java @@ -0,0 +1,80 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * A custom implementation of OkHttp's RequestBody that wraps an InputStream. + * This class allows streaming of data from an InputStream directly to an HTTP request body, + * which is useful for file uploads or sending large amounts of data without loading it all into memory. + */ +public class InputStreamRequestBody extends RequestBody { + private final InputStream inputStream; + private final MediaType contentType; + + /** + * Constructs an InputStreamRequestBody with the specified content type and input stream. + * + * @param contentType the MediaType of the content, or null if not known + * @param inputStream the InputStream containing the data to be sent + * @throws NullPointerException if inputStream is null + */ + public InputStreamRequestBody(@Nullable MediaType contentType, InputStream inputStream) { + this.contentType = contentType; + this.inputStream = Objects.requireNonNull(inputStream, "inputStream == null"); + } + + /** + * Returns the content type of this request body. + * + * @return the MediaType of the content, or null if not specified + */ + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + /** + * Returns the content length of this request body, if known. + * This method attempts to determine the length using the InputStream's available() method, + * which may not always accurately reflect the total length of the stream. + * + * @return the content length, or -1 if the length is unknown + * @throws IOException if an I/O error occurs + */ + @Override + public long contentLength() throws IOException { + return inputStream.available() == 0 ? -1 : inputStream.available(); + } + + /** + * Writes the content of the InputStream to the given BufferedSink. + * This method is responsible for transferring the data from the InputStream to the network request. + * + * @param sink the BufferedSink to write the content to + * @throws IOException if an I/O error occurs during writing + */ + @Override + public void writeTo(BufferedSink sink) throws IOException { + Source source = null; + try { + source = Okio.source(inputStream); + sink.writeAll(source); + } finally { + Util.closeQuietly(Objects.requireNonNull(source)); + } + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java new file mode 100644 index 00000000..11714cb8 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/MediaTypes.java @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.MediaType; + +public final class MediaTypes { + + public static final MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + private MediaTypes() {} +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java new file mode 100644 index 00000000..5929c12d --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/Nullable.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import java.util.Optional; +import java.util.function.Function; + +public final class Nullable { + + private final Either, Null> value; + + private Nullable() { + this.value = Either.left(Optional.empty()); + } + + private Nullable(T value) { + if (value == null) { + this.value = Either.right(Null.INSTANCE); + } else { + this.value = Either.left(Optional.of(value)); + } + } + + public static Nullable ofNull() { + return new Nullable<>(null); + } + + public static Nullable of(T value) { + return new Nullable<>(value); + } + + public static Nullable empty() { + return new Nullable<>(); + } + + public static Nullable ofOptional(Optional value) { + if (value.isPresent()) { + return of(value.get()); + } else { + return empty(); + } + } + + public boolean isNull() { + return this.value.isRight(); + } + + public boolean isEmpty() { + return this.value.isLeft() && !this.value.getLeft().isPresent(); + } + + public T get() { + if (this.isNull()) { + return null; + } + + return this.value.getLeft().get(); + } + + public Nullable map(Function mapper) { + if (this.isNull()) { + return Nullable.ofNull(); + } + + return Nullable.ofOptional(this.value.getLeft().map(mapper)); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Nullable)) { + return false; + } + + if (((Nullable) other).isNull() && this.isNull()) { + return true; + } + + return this.value.getLeft().equals(((Nullable) other).value.getLeft()); + } + + private static final class Either { + private L left = null; + private R right = null; + + private Either(L left, R right) { + if (left != null && right != null) { + throw new IllegalArgumentException("Left and right argument cannot both be non-null."); + } + + if (left == null && right == null) { + throw new IllegalArgumentException("Left and right argument cannot both be null."); + } + + if (left != null) { + this.left = left; + } + + if (right != null) { + this.right = right; + } + } + + public static Either left(L left) { + return new Either<>(left, null); + } + + public static Either right(R right) { + return new Either<>(null, right); + } + + public boolean isLeft() { + return this.left != null; + } + + public boolean isRight() { + return this.right != null; + } + + public L getLeft() { + if (!this.isLeft()) { + throw new IllegalArgumentException("Cannot get left from right Either."); + } + return this.left; + } + + public R getRight() { + if (!this.isRight()) { + throw new IllegalArgumentException("Cannot get right from left Either."); + } + return this.right; + } + } + + private static final class Null { + private static final Null INSTANCE = new Null(); + + private Null() {} + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java new file mode 100644 index 00000000..98c33be4 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/NullableNonemptyFilter.java @@ -0,0 +1,19 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import java.util.Optional; + +public final class NullableNonemptyFilter { + @Override + public boolean equals(Object o) { + boolean isOptionalEmpty = isOptionalEmpty(o); + + return isOptionalEmpty; + } + + private boolean isOptionalEmpty(Object o) { + return o instanceof Optional && !((Optional) o).isPresent(); + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java new file mode 100644 index 00000000..acec32b4 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ObjectMappers.java @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.io.IOException; + +public final class ObjectMappers { + public static final ObjectMapper JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new JavaTimeModule()) + .addModule(DateTimeDeserializer.getModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + + private ObjectMappers() {} + + public static String stringify(Object o) { + try { + return JSON_MAPPER + .setSerializationInclusion(JsonInclude.Include.ALWAYS) + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } catch (IOException e) { + return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode()); + } + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java new file mode 100644 index 00000000..c0687736 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/QueryStringMapper.java @@ -0,0 +1,139 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.HttpUrl; +import okhttp3.MultipartBody; + +import java.util.*; + +public class QueryStringMapper { + + private static final ObjectMapper MAPPER = ObjectMappers.JSON_MAPPER; + + public static void addQueryParameter(HttpUrl.Builder httpUrl, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + httpUrl.addQueryParameter(key, valueNode.textValue()); + } else { + httpUrl.addQueryParameter(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().textValue()); + } else { + httpUrl.addQueryParameter(key + field.getKey(), field.getValue().toString()); + } + } + } + + public static void addFormDataPart( + MultipartBody.Builder multipartBody, String key, Object value, boolean arraysAsRepeats) { + JsonNode valueNode = MAPPER.valueToTree(value); + + List> flat; + if (valueNode.isObject()) { + flat = flattenObject((ObjectNode) valueNode, arraysAsRepeats); + } else if (valueNode.isArray()) { + flat = flattenArray((ArrayNode) valueNode, "", arraysAsRepeats); + } else { + if (valueNode.isTextual()) { + multipartBody.addFormDataPart(key, valueNode.textValue()); + } else { + multipartBody.addFormDataPart(key, valueNode.toString()); + } + return; + } + + for (Map.Entry field : flat) { + if (field.getValue().isTextual()) { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().textValue()); + } else { + multipartBody.addFormDataPart( + key + field.getKey(), field.getValue().toString()); + } + } + } + + public static List> flattenObject(ObjectNode object, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator> fields = object.fields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + + String key = "[" + field.getKey() + "]"; + + if (field.getValue().isObject()) { + List> flatField = + flattenObject((ObjectNode) field.getValue(), arraysAsRepeats); + addAll(flat, flatField, key); + } else if (field.getValue().isArray()) { + List> flatField = + flattenArray((ArrayNode) field.getValue(), key, arraysAsRepeats); + addAll(flat, flatField, ""); + } else { + flat.add(new AbstractMap.SimpleEntry<>(key, field.getValue())); + } + } + + return flat; + } + + private static List> flattenArray( + ArrayNode array, String key, boolean arraysAsRepeats) { + List> flat = new ArrayList<>(); + + Iterator elements = array.elements(); + + int index = 0; + while (elements.hasNext()) { + JsonNode element = elements.next(); + + String indexKey = key + "[" + index + "]"; + + if (arraysAsRepeats) { + indexKey = key; + } + + if (element.isObject()) { + List> flatField = flattenObject((ObjectNode) element, arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else if (element.isArray()) { + List> flatField = flattenArray((ArrayNode) element, "", arraysAsRepeats); + addAll(flat, flatField, indexKey); + } else { + flat.add(new AbstractMap.SimpleEntry<>(indexKey, element)); + } + + index++; + } + + return flat; + } + + private static void addAll( + List> target, List> source, String prefix) { + for (Map.Entry entry : source) { + Map.Entry entryToAdd = + new AbstractMap.SimpleEntry<>(prefix + entry.getKey(), entry.getValue()); + target.add(entryToAdd); + } + } +} diff --git a/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 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/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java new file mode 100644 index 00000000..1bb0b5dc --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyInputStream.java @@ -0,0 +1,46 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.Response; + +import java.io.FilterInputStream; +import java.io.IOException; + +/** + * A custom InputStream that wraps the InputStream from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the stream is closed. + * + * This class extends FilterInputStream and takes an OkHttp Response object as a parameter. + * It retrieves the InputStream from the Response and overrides the close method to close + * both the InputStream and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyInputStream extends FilterInputStream { + private final Response response; + + /** + * Constructs a ResponseBodyInputStream that wraps the InputStream from the given OkHttp + * Response object. + * + * @param response the OkHttp Response object from which the InputStream is retrieved + * @throws IOException if an I/O error occurs while retrieving the InputStream + */ + public ResponseBodyInputStream(Response response) throws IOException { + super(response.body().byteStream()); + this.response = response; + } + + /** + * Closes the InputStream and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the stream is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the stream is closed + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java new file mode 100644 index 00000000..e6c1a525 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/ResponseBodyReader.java @@ -0,0 +1,45 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.Response; + +import java.io.FilterReader; +import java.io.IOException; + +/** + * A custom Reader that wraps the Reader from the OkHttp Response and ensures that the + * OkHttp Response object is properly closed when the reader is closed. + * + * This class extends FilterReader and takes an OkHttp Response object as a parameter. + * It retrieves the Reader from the Response and overrides the close method to close + * both the Reader and the Response object, ensuring proper resource management and preventing + * premature closure of the underlying HTTP connection. + */ +public class ResponseBodyReader extends FilterReader { + private final Response response; + + /** + * Constructs a ResponseBodyReader that wraps the Reader from the given OkHttp Response object. + * + * @param response the OkHttp Response object from which the Reader is retrieved + * @throws IOException if an I/O error occurs while retrieving the Reader + */ + public ResponseBodyReader(Response response) throws IOException { + super(response.body().charStream()); + this.response = response; + } + + /** + * Closes the Reader and the associated OkHttp Response object. This ensures that the + * underlying HTTP connection is properly closed after the reader is no longer needed. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + super.close(); + response.close(); // Ensure the response is closed when the reader is closed + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java new file mode 100644 index 00000000..7a28c3c9 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/RetryInterceptor.java @@ -0,0 +1,79 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import okhttp3.Interceptor; +import okhttp3.Response; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import java.util.Random; + +public class RetryInterceptor implements Interceptor { + + private static final Duration ONE_SECOND = Duration.ofSeconds(1); + private final ExponentialBackoff backoff; + private final Random random = new Random(); + + public RetryInterceptor(int maxRetries) { + this.backoff = new ExponentialBackoff(maxRetries); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (shouldRetry(response.code())) { + return retryChain(response, chain); + } + + return response; + } + + private Response retryChain(Response response, Chain chain) throws IOException { + Optional nextBackoff = this.backoff.nextBackoff(); + while (nextBackoff.isPresent()) { + try { + Thread.sleep(nextBackoff.get().toMillis()); + } catch (InterruptedException e) { + throw new IOException("Interrupted while trying request", e); + } + response.close(); + response = chain.proceed(chain.request()); + if (shouldRetry(response.code())) { + nextBackoff = this.backoff.nextBackoff(); + } else { + return response; + } + } + + return response; + } + + private static boolean shouldRetry(int statusCode) { + return statusCode == 408 || statusCode == 429 || statusCode >= 500; + } + + private final class ExponentialBackoff { + + private final int maxNumRetries; + + private int retryNumber = 0; + + ExponentialBackoff(int maxNumRetries) { + this.maxNumRetries = maxNumRetries; + } + + public Optional nextBackoff() { + retryNumber += 1; + if (retryNumber > maxNumRetries) { + return Optional.empty(); + } + + int upperBound = (int) Math.pow(2, retryNumber); + return Optional.of(ONE_SECOND.multipliedBy(random.nextInt(upperBound))); + } + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/Stream.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Stream.java new file mode 100644 index 00000000..f037712a --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/Stream.java @@ -0,0 +1,97 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +/** + * The {@code Stream} class implements {@link Iterable} to provide a simple mechanism for reading and parsing + * objects of a given type from data streamed via a {@link Reader} using a specified delimiter. + *

+ * {@code Stream} assumes that data is being pushed to the provided {@link Reader} asynchronously and utilizes a + * {@code Scanner} to block during iteration if the next object is not available. + * + * @param The type of objects in the stream. + */ +public final class Stream implements Iterable { + /** + * The {@link Class} of the objects in the stream. + */ + private final Class valueType; + /** + * The {@link Scanner} used for reading from the input stream and blocking when needed during iteration. + */ + private final Scanner scanner; + + /** + * Constructs a new {@code Stream} with the specified value type, reader, and delimiter. + * + * @param valueType The class of the objects in the stream. + * @param reader The reader that provides the streamed data. + * @param delimiter The delimiter used to separate elements in the stream. + */ + public Stream(Class valueType, Reader reader, String delimiter) { + this.scanner = new Scanner(reader).useDelimiter(delimiter); + this.valueType = valueType; + } + + /** + * Returns an iterator over the elements in this stream that blocks during iteration when the next object is + * not yet available. + * + * @return An iterator that can be used to traverse the elements in the stream. + */ + @Override + public Iterator iterator() { + return new Iterator() { + /** + * Returns {@code true} if there are more elements in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + */ + @Override + public boolean hasNext() { + return scanner.hasNext(); + } + + /** + * Returns the next element in the stream. + *

+ * Will block and wait for input if the stream has not ended and the next object is not yet available. + * + * @return The next element in the stream. + * @throws NoSuchElementException If there are no more elements in the stream. + */ + @Override + public T next() { + if (!scanner.hasNext()) { + throw new NoSuchElementException(); + } else { + try { + T parsedResponse = ObjectMappers.JSON_MAPPER.readValue( + scanner.next().trim(), valueType); + return parsedResponse; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Removing elements from {@code Stream} is not supported. + * + * @throws UnsupportedOperationException Always, as removal is not supported. + */ + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/skyvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java b/skyvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java new file mode 100644 index 00000000..307d5852 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/generated/rest/core/Suppliers.java @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.skyflow.generated.rest.core; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public final class Suppliers { + private Suppliers() {} + + public static Supplier memoize(Supplier delegate) { + AtomicReference value = new AtomicReference<>(); + return () -> { + T val = value.get(); + if (val == null) { + val = value.updateAndGet(cur -> cur == null ? Objects.requireNonNull(delegate.get()) : cur); + } + return val; + }; + } +} diff --git a/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 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/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 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/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 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/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 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/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 99% rename from 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 index 0064558d..7e2719f2 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncAuditClient.java @@ -7,6 +7,7 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.audit.requests.AuditServiceListAuditEventsRequest; import com.skyflow.generated.rest.types.V1AuditResponse; + import java.util.concurrent.CompletableFuture; public class AsyncAuditClient { diff --git a/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 95% rename from 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 index 875ca259..e1516eaf 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/AsyncRawAuditClient.java @@ -4,27 +4,15 @@ package com.skyflow.generated.rest.resources.audit; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.audit.requests.AuditServiceListAuditEventsRequest; import com.skyflow.generated.rest.types.V1AuditResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawAuditClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 95% rename from 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 index bfec3bf2..68cd5e3b 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/audit/RawAuditClient.java @@ -4,23 +4,13 @@ package com.skyflow.generated.rest.resources.audit; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.audit.requests.AuditServiceListAuditEventsRequest; import com.skyflow.generated.rest.types.V1AuditResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawAuditClient { protected final ClientOptions clientOptions; diff --git a/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 98% rename from 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 index 8b6686e4..e9f8cf12 100644 --- a/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 @@ -3,26 +3,16 @@ */ package com.skyflow.generated.rest.resources.audit.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestFilterOpsActionType; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestFilterOpsContextAccessType; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestFilterOpsContextActorType; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestFilterOpsContextAuthMode; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestFilterOpsResourceType; -import com.skyflow.generated.rest.resources.audit.types.AuditServiceListAuditEventsRequestSortOpsOrderBy; +import com.skyflow.generated.rest.resources.audit.types.*; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = AuditServiceListAuditEventsRequest.Builder.class) diff --git a/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 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/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 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/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 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/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 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/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 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/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 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/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 99% rename from 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 index f10ed979..fb2d07d0 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncBinLookupClient.java @@ -7,6 +7,7 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.binlookup.requests.V1BinListRequest; import com.skyflow.generated.rest.types.V1BinListResponse; + import java.util.concurrent.CompletableFuture; public class AsyncBinLookupClient { diff --git a/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 88% rename from 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 index 2a3f5f4f..9cf03c7d 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/AsyncRawBinLookupClient.java @@ -4,28 +4,15 @@ package com.skyflow.generated.rest.resources.binlookup; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.binlookup.requests.V1BinListRequest; import com.skyflow.generated.rest.types.V1BinListResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawBinLookupClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 87% rename from 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 index bb08039b..21e77017 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/binlookup/RawBinLookupClient.java @@ -4,24 +4,13 @@ package com.skyflow.generated.rest.resources.binlookup; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.binlookup.requests.V1BinListRequest; import com.skyflow.generated.rest.types.V1BinListResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawBinLookupClient { protected final ClientOptions clientOptions; diff --git a/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 91% rename from 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 index af827e01..def40293 100644 --- a/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 @@ -3,21 +3,12 @@ */ package com.skyflow.generated.rest.resources.binlookup.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1VaultSchemaConfig; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1BinListRequest.Builder.class) diff --git a/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 92% rename from 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 index 1f157223..01031423 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncFilesClient.java @@ -5,20 +5,11 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileAudioRequestDeidentifyAudio; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileDocumentPdfRequestDeidentifyPdf; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileImageRequestDeidentifyImage; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequest; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyDocument; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyPresentation; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifySpreadsheet; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyStructuredText; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText; -import com.skyflow.generated.rest.resources.files.requests.GetRunRequest; -import com.skyflow.generated.rest.resources.files.requests.ReidentifyFileRequestReidentifyFile; +import com.skyflow.generated.rest.resources.files.requests.*; import com.skyflow.generated.rest.types.DeidentifyFileResponse; import com.skyflow.generated.rest.types.DetectRunsResponse; import com.skyflow.generated.rest.types.ReidentifyFileResponse; + import java.util.concurrent.CompletableFuture; public class AsyncFilesClient { diff --git a/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 96% rename from 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 index 014f3cf8..e7f008e8 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/AsyncRawFilesClient.java @@ -4,44 +4,20 @@ package com.skyflow.generated.rest.resources.files; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileAudioRequestDeidentifyAudio; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileDocumentPdfRequestDeidentifyPdf; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileImageRequestDeidentifyImage; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequest; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyDocument; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyPresentation; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifySpreadsheet; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyStructuredText; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText; -import com.skyflow.generated.rest.resources.files.requests.GetRunRequest; -import com.skyflow.generated.rest.resources.files.requests.ReidentifyFileRequestReidentifyFile; +import com.skyflow.generated.rest.resources.files.requests.*; import com.skyflow.generated.rest.types.DeidentifyFileResponse; import com.skyflow.generated.rest.types.DetectRunsResponse; import com.skyflow.generated.rest.types.ErrorResponse; import com.skyflow.generated.rest.types.ReidentifyFileResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawFilesClient { protected final ClientOptions clientOptions; diff --git a/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 91% rename from 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 index 8d083d76..bcda96e6 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/FilesClient.java @@ -5,17 +5,7 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileAudioRequestDeidentifyAudio; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileDocumentPdfRequestDeidentifyPdf; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileImageRequestDeidentifyImage; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequest; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyDocument; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyPresentation; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifySpreadsheet; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyStructuredText; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText; -import com.skyflow.generated.rest.resources.files.requests.GetRunRequest; -import com.skyflow.generated.rest.resources.files.requests.ReidentifyFileRequestReidentifyFile; +import com.skyflow.generated.rest.resources.files.requests.*; import com.skyflow.generated.rest.types.DeidentifyFileResponse; import com.skyflow.generated.rest.types.DetectRunsResponse; import com.skyflow.generated.rest.types.ReidentifyFileResponse; diff --git a/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 96% rename from 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 index 164c4d18..d277d224 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/files/RawFilesClient.java @@ -4,40 +4,18 @@ package com.skyflow.generated.rest.resources.files; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileAudioRequestDeidentifyAudio; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileDocumentPdfRequestDeidentifyPdf; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileImageRequestDeidentifyImage; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequest; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyDocument; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyPresentation; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifySpreadsheet; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyStructuredText; -import com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText; -import com.skyflow.generated.rest.resources.files.requests.GetRunRequest; -import com.skyflow.generated.rest.resources.files.requests.ReidentifyFileRequestReidentifyFile; +import com.skyflow.generated.rest.resources.files.requests.*; import com.skyflow.generated.rest.types.DeidentifyFileResponse; import com.skyflow.generated.rest.types.DetectRunsResponse; import com.skyflow.generated.rest.types.ErrorResponse; import com.skyflow.generated.rest.types.ReidentifyFileResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawFilesClient { protected final ClientOptions clientOptions; diff --git a/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 98% rename from 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 index d1cadca3..c2baa931 100644 --- a/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 @@ -3,13 +3,7 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileAudioRequestDeidentifyAudioEntityTypesItem; @@ -17,13 +11,10 @@ import com.skyflow.generated.rest.types.FileDataDeidentifyAudio; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileAudioRequestDeidentifyAudio.Builder.class) public final class DeidentifyFileAudioRequestDeidentifyAudio { diff --git a/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 97% rename from 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 index 5b6a653f..1844a116 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileDocumentPdfRequestDeidentifyPdfEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifyPdf; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileDocumentPdfRequestDeidentifyPdf.Builder.class) public final class DeidentifyFileDocumentPdfRequestDeidentifyPdf { diff --git a/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 97% rename from 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 index 015bba9e..25ddea9c 100644 --- a/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 @@ -3,13 +3,7 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileImageRequestDeidentifyImageEntityTypesItem; @@ -17,13 +11,10 @@ import com.skyflow.generated.rest.types.FileDataDeidentifyImage; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileImageRequestDeidentifyImage.Builder.class) public final class DeidentifyFileImageRequestDeidentifyImage { diff --git a/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 97% rename from 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 index c2cdcfb5..62d77229 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestEntityTypesItem; import com.skyflow.generated.rest.types.FileData; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequest.Builder.class) public final class DeidentifyFileRequest { diff --git a/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 97% rename from 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 index fc3db52a..c4d9841d 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestDeidentifyDocumentEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifyDocument; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequestDeidentifyDocument.Builder.class) public final class DeidentifyFileRequestDeidentifyDocument { diff --git a/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 97% rename from 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 index f45802ec..e1cea77f 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestDeidentifyPresentationEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifyPresentation; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequestDeidentifyPresentation.Builder.class) public final class DeidentifyFileRequestDeidentifyPresentation { diff --git a/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 97% rename from 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 index 93ea921a..10ca8feb 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestDeidentifySpreadsheetEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifySpreadsheet; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequestDeidentifySpreadsheet.Builder.class) public final class DeidentifyFileRequestDeidentifySpreadsheet { diff --git a/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 97% rename from 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 index b579fe1b..e3f555b8 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestDeidentifyStructuredTextEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifyStructuredText; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequestDeidentifyStructuredText.Builder.class) public final class DeidentifyFileRequestDeidentifyStructuredText { diff --git a/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 97% rename from 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 index 7a7c6615..8457c790 100644 --- a/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 @@ -3,26 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.files.types.DeidentifyFileRequestDeidentifyTextEntityTypesItem; import com.skyflow.generated.rest.types.FileDataDeidentifyText; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyFileRequestDeidentifyText.Builder.class) public final class DeidentifyFileRequestDeidentifyText { diff --git a/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 86% rename from 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 index 490392f1..cde24227 100644 --- a/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 @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 92% rename from 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 index 0f5e79ab..5cd22747 100644 --- a/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 @@ -3,22 +3,17 @@ */ package com.skyflow.generated.rest.resources.files.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.FileDataReidentifyFile; import com.skyflow.generated.rest.types.Format; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ReidentifyFileRequestReidentifyFile.Builder.class) diff --git a/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 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/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 99% rename from 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 index af874ecd..fa45ee60 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncGuardrailsClient.java @@ -7,6 +7,7 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.guardrails.requests.DetectGuardrailsRequest; import com.skyflow.generated.rest.types.DetectGuardrailsResponse; + import java.util.concurrent.CompletableFuture; public class AsyncGuardrailsClient { diff --git a/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 89% rename from 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 index 0088d7a3..80853472 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/AsyncRawGuardrailsClient.java @@ -4,31 +4,18 @@ package com.skyflow.generated.rest.resources.guardrails; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; import com.skyflow.generated.rest.resources.guardrails.requests.DetectGuardrailsRequest; import com.skyflow.generated.rest.types.DetectGuardrailsResponse; import com.skyflow.generated.rest.types.ErrorResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawGuardrailsClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 87% rename from 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 index 0223134b..5bf5c31a 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/guardrails/RawGuardrailsClient.java @@ -4,27 +4,16 @@ package com.skyflow.generated.rest.resources.guardrails; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; import com.skyflow.generated.rest.resources.guardrails.requests.DetectGuardrailsRequest; import com.skyflow.generated.rest.types.DetectGuardrailsResponse; import com.skyflow.generated.rest.types.ErrorResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawGuardrailsClient { protected final ClientOptions clientOptions; diff --git a/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 93% rename from 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 index 9063fed8..75250f8c 100644 --- a/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 @@ -3,22 +3,13 @@ */ package com.skyflow.generated.rest.resources.guardrails.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DetectGuardrailsRequest.Builder.class) public final class DetectGuardrailsRequest { diff --git a/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 99% rename from 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 index 894c97b5..cab967a7 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncQueryClient.java @@ -7,6 +7,7 @@ import com.skyflow.generated.rest.core.RequestOptions; import com.skyflow.generated.rest.resources.query.requests.QueryServiceExecuteQueryBody; import com.skyflow.generated.rest.types.V1GetQueryResponse; + import java.util.concurrent.CompletableFuture; public class AsyncQueryClient { diff --git a/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 91% rename from 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 index 96e2dc0b..7b8c9383 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/AsyncRawQueryClient.java @@ -4,28 +4,15 @@ package com.skyflow.generated.rest.resources.query; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.query.requests.QueryServiceExecuteQueryBody; import com.skyflow.generated.rest.types.V1GetQueryResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawQueryClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 91% rename from 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 index 3e5bee51..ec88ae93 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/query/RawQueryClient.java @@ -4,24 +4,13 @@ package com.skyflow.generated.rest.resources.query; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.query.requests.QueryServiceExecuteQueryBody; import com.skyflow.generated.rest.types.V1GetQueryResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawQueryClient { protected final ClientOptions clientOptions; diff --git a/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 94% rename from 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 index 60565eb5..000cd685 100644 --- a/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 @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.resources.query.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 96% rename from 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 index a810f142..b1c9ae18 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRawRecordsClient.java @@ -4,54 +4,21 @@ package com.skyflow.generated.rest.resources.records; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.records.requests.FileServiceUploadFileRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; -import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; -import com.skyflow.generated.rest.types.ErrorResponse; -import com.skyflow.generated.rest.types.UploadFileV2Response; -import com.skyflow.generated.rest.types.V1BatchOperationResponse; -import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; -import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; -import com.skyflow.generated.rest.types.V1DeleteFileResponse; -import com.skyflow.generated.rest.types.V1DeleteRecordResponse; -import com.skyflow.generated.rest.types.V1FieldRecords; -import com.skyflow.generated.rest.types.V1GetFileScanStatusResponse; -import com.skyflow.generated.rest.types.V1InsertRecordResponse; -import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.generated.rest.resources.records.requests.*; +import com.skyflow.generated.rest.types.*; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawRecordsClient { protected final ClientOptions clientOptions; diff --git a/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 91% rename from 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 index c6925b50..ed7240c9 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/AsyncRecordsClient.java @@ -5,24 +5,9 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.records.requests.FileServiceUploadFileRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; -import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; -import com.skyflow.generated.rest.types.UploadFileV2Response; -import com.skyflow.generated.rest.types.V1BatchOperationResponse; -import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; -import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; -import com.skyflow.generated.rest.types.V1DeleteFileResponse; -import com.skyflow.generated.rest.types.V1DeleteRecordResponse; -import com.skyflow.generated.rest.types.V1FieldRecords; -import com.skyflow.generated.rest.types.V1GetFileScanStatusResponse; -import com.skyflow.generated.rest.types.V1InsertRecordResponse; -import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.generated.rest.resources.records.requests.*; +import com.skyflow.generated.rest.types.*; + import java.io.File; import java.util.Optional; import java.util.concurrent.CompletableFuture; diff --git a/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 95% rename from 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 index d1b607a0..29530b56 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RawRecordsClient.java @@ -4,50 +4,19 @@ package com.skyflow.generated.rest.resources.records; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.QueryStringMapper; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.errors.UnauthorizedError; -import com.skyflow.generated.rest.resources.records.requests.FileServiceUploadFileRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; -import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; -import com.skyflow.generated.rest.types.ErrorResponse; -import com.skyflow.generated.rest.types.UploadFileV2Response; -import com.skyflow.generated.rest.types.V1BatchOperationResponse; -import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; -import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; -import com.skyflow.generated.rest.types.V1DeleteFileResponse; -import com.skyflow.generated.rest.types.V1DeleteRecordResponse; -import com.skyflow.generated.rest.types.V1FieldRecords; -import com.skyflow.generated.rest.types.V1GetFileScanStatusResponse; -import com.skyflow.generated.rest.types.V1InsertRecordResponse; -import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.generated.rest.resources.records.requests.*; +import com.skyflow.generated.rest.types.*; +import okhttp3.*; + import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Optional; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawRecordsClient { protected final ClientOptions clientOptions; diff --git a/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 90% rename from 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 index 7b4599c6..eb9bc81d 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/records/RecordsClient.java @@ -5,24 +5,9 @@ import com.skyflow.generated.rest.core.ClientOptions; import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.records.requests.FileServiceUploadFileRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceGetRecordRequest; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; -import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; -import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; -import com.skyflow.generated.rest.types.UploadFileV2Response; -import com.skyflow.generated.rest.types.V1BatchOperationResponse; -import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; -import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; -import com.skyflow.generated.rest.types.V1DeleteFileResponse; -import com.skyflow.generated.rest.types.V1DeleteRecordResponse; -import com.skyflow.generated.rest.types.V1FieldRecords; -import com.skyflow.generated.rest.types.V1GetFileScanStatusResponse; -import com.skyflow.generated.rest.types.V1InsertRecordResponse; -import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.generated.rest.resources.records.requests.*; +import com.skyflow.generated.rest.types.*; + import java.io.File; import java.util.Optional; diff --git a/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 88% rename from 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 index fe805970..3efb0a98 100644 --- a/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 @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 90% rename from 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 index 48586839..c1997213 100644 --- a/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 @@ -3,22 +3,13 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1BatchRecord; import com.skyflow.generated.rest.types.V1Byot; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = RecordServiceBatchOperationBody.Builder.class) diff --git a/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 85% rename from 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 index b417adbb..4b9dcced 100644 --- a/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 @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = RecordServiceBulkDeleteRecordBody.Builder.class) diff --git a/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 96% rename from 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 index 22c9090c..9b59168a 100644 --- a/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 @@ -3,23 +3,13 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestOrderBy; import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestRedaction; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = RecordServiceBulkGetRecordRequest.Builder.class) diff --git a/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 92% rename from 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 index eef17665..91317d02 100644 --- a/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 @@ -3,22 +3,12 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.records.types.RecordServiceGetRecordRequestRedaction; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = RecordServiceGetRecordRequest.Builder.class) diff --git a/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 93% rename from 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 index e1de00f7..e832e396 100644 --- a/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 @@ -3,22 +3,13 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1Byot; import com.skyflow.generated.rest.types.V1FieldRecords; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = RecordServiceInsertRecordBody.Builder.class) diff --git a/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 92% rename from 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 index e52af2e4..f5c7a229 100644 --- a/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 @@ -3,17 +3,12 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1Byot; import com.skyflow.generated.rest.types.V1FieldRecords; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 95% rename from 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 index f162530c..81a9f837 100644 --- a/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 @@ -3,20 +3,15 @@ */ package com.skyflow.generated.rest.resources.records.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = UploadFileV2Request.Builder.class) diff --git a/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 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/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 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/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 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/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 94% rename from 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 index 96428263..36b364b5 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncRawStringsClient.java @@ -4,13 +4,7 @@ package com.skyflow.generated.rest.resources.strings; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; @@ -19,18 +13,11 @@ import com.skyflow.generated.rest.types.DeidentifyStringResponse; import com.skyflow.generated.rest.types.ErrorResponse; import com.skyflow.generated.rest.types.IdentifyResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawStringsClient { protected final ClientOptions clientOptions; diff --git a/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 99% rename from 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 index 046bb289..66ef72c5 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/AsyncStringsClient.java @@ -9,6 +9,7 @@ import com.skyflow.generated.rest.resources.strings.requests.ReidentifyStringRequest; import com.skyflow.generated.rest.types.DeidentifyStringResponse; import com.skyflow.generated.rest.types.IdentifyResponse; + import java.util.concurrent.CompletableFuture; public class AsyncStringsClient { diff --git a/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 92% rename from 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 index a5e16949..da6bec56 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/strings/RawStringsClient.java @@ -4,13 +4,7 @@ package com.skyflow.generated.rest.resources.strings; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.BadRequestError; import com.skyflow.generated.rest.errors.InternalServerError; import com.skyflow.generated.rest.errors.UnauthorizedError; @@ -19,14 +13,9 @@ import com.skyflow.generated.rest.types.DeidentifyStringResponse; import com.skyflow.generated.rest.types.ErrorResponse; import com.skyflow.generated.rest.types.IdentifyResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawStringsClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 97% rename from 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 index 943ddde4..06ec6cec 100644 --- a/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 @@ -3,25 +3,16 @@ */ package com.skyflow.generated.rest.resources.strings.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.resources.strings.types.DeidentifyStringRequestEntityTypesItem; import com.skyflow.generated.rest.types.TokenTypeMapping; import com.skyflow.generated.rest.types.Transformations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyStringRequest.Builder.class) public final class DeidentifyStringRequest { diff --git a/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 91% rename from 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 index 59c1aaf2..3c42390c 100644 --- a/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 @@ -3,16 +3,11 @@ */ package com.skyflow.generated.rest.resources.strings.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.Format; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/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 94% rename from 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 index fbf69580..794035bd 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncRawTokensClient.java @@ -4,30 +4,17 @@ package com.skyflow.generated.rest.resources.tokens; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.tokens.requests.V1DetokenizePayload; import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; import com.skyflow.generated.rest.types.V1DetokenizeResponse; import com.skyflow.generated.rest.types.V1TokenizeResponse; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; public class AsyncRawTokensClient { protected final ClientOptions clientOptions; diff --git a/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 99% rename from 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 index 26d5ea05..621701af 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/AsyncTokensClient.java @@ -9,6 +9,7 @@ import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; import com.skyflow.generated.rest.types.V1DetokenizeResponse; import com.skyflow.generated.rest.types.V1TokenizeResponse; + import java.util.concurrent.CompletableFuture; public class AsyncTokensClient { diff --git a/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 93% rename from 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 index 5ed17d28..7ad69d11 100644 --- a/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/resources/tokens/RawTokensClient.java @@ -4,26 +4,15 @@ package com.skyflow.generated.rest.resources.tokens; import com.fasterxml.jackson.core.JsonProcessingException; -import com.skyflow.generated.rest.core.ApiClientApiException; -import com.skyflow.generated.rest.core.ApiClientException; -import com.skyflow.generated.rest.core.ApiClientHttpResponse; -import com.skyflow.generated.rest.core.ClientOptions; -import com.skyflow.generated.rest.core.MediaTypes; -import com.skyflow.generated.rest.core.ObjectMappers; -import com.skyflow.generated.rest.core.RequestOptions; +import com.skyflow.generated.rest.core.*; import com.skyflow.generated.rest.errors.NotFoundError; import com.skyflow.generated.rest.resources.tokens.requests.V1DetokenizePayload; import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; import com.skyflow.generated.rest.types.V1DetokenizeResponse; import com.skyflow.generated.rest.types.V1TokenizeResponse; +import okhttp3.*; + import java.io.IOException; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; public class RawTokensClient { protected final ClientOptions clientOptions; diff --git a/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 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/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 91% rename from 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 index 59318aa8..52571f64 100644 --- a/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 @@ -3,21 +3,12 @@ */ package com.skyflow.generated.rest.resources.tokens.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1DetokenizeRecordRequest; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1DetokenizePayload.Builder.class) diff --git a/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 85% rename from 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 index 9729023e..e5fc08f4 100644 --- a/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 @@ -3,21 +3,12 @@ */ package com.skyflow.generated.rest.resources.tokens.requests; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; import com.skyflow.generated.rest.types.V1TokenizeRecordRequest; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1TokenizePayload.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java similarity index 97% rename from src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java index 210f96c0..73aed9c1 100644 --- a/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventContext.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java similarity index 87% rename from src/main/java/com/skyflow/generated/rest/types/AuditEventData.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java index e2028fba..bab4b42a 100644 --- a/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventData.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java similarity index 89% rename from src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java index 1352c42f..9d3bbaed 100644 --- a/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/AuditEventHttpInfo.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java similarity index 93% rename from src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java index a7dd7c68..dc244be8 100644 --- a/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifiedFileOutput.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java similarity index 87% rename from src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java index d2b8e5b6..d5a2ebd5 100644 --- a/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyFileResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java similarity index 91% rename from src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java index e831f142..1389fa0f 100644 --- a/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/DeidentifyStringResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DeidentifyStringResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java similarity index 94% rename from src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java index 9773dcc3..9cb2d319 100644 --- a/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectGuardrailsResponse.java @@ -3,20 +3,15 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DetectGuardrailsResponse.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java similarity index 95% rename from src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java index c97f7635..b73f7845 100644 --- a/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/DetectRunsResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = DetectRunsResponse.Builder.class) diff --git a/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 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/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java similarity index 88% rename from src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java index 565714eb..4113b998 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponse.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ErrorResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java index bcc23f03..4774fa57 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ErrorResponseError.java @@ -3,22 +3,13 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; import org.jetbrains.annotations.NotNull; +import java.util.*; + @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ErrorResponseError.Builder.class) public final class ErrorResponseError { diff --git a/src/main/java/com/skyflow/generated/rest/types/FileData.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileData.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileData.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileData.java index 7f58332e..fd6fa822 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileData.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileData.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileData.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java index 2aa88f7b..b6a5ea11 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyAudio.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyAudio.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java index e6390e28..5fb12fba 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyDocument.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyDocument.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java index 127c5f4c..bc5e2c6b 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyImage.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyImage.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java index 20b0314f..5606e917 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPdf.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyPdf.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java index e808f0b2..0bedabc5 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyPresentation.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyPresentation.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java index 33478d20..dd017bb8 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifySpreadsheet.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifySpreadsheet.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java index dad188ce..814d51c3 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyStructuredText.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyStructuredText.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java index 79045d7a..e5c88180 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataDeidentifyText.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataDeidentifyText.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java index edea3145..9148703c 100644 --- a/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/FileDataReidentifyFile.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = FileDataReidentifyFile.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/Format.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Format.java similarity index 89% rename from src/main/java/com/skyflow/generated/rest/types/Format.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Format.java index 68ac94ca..1563b90a 100644 --- a/src/main/java/com/skyflow/generated/rest/types/Format.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/Format.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = Format.Builder.class) diff --git a/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 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/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java similarity index 88% rename from src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java index 807aee4e..55be5699 100644 --- a/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/GooglerpcStatus.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = GooglerpcStatus.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java similarity index 89% rename from src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java index e7ae30fc..c1bd23c6 100644 --- a/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/IdentifyResponse.java @@ -3,18 +3,14 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; +import org.jetbrains.annotations.NotNull; + import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = IdentifyResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/Locations.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Locations.java similarity index 94% rename from src/main/java/com/skyflow/generated/rest/types/Locations.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Locations.java index 9864541a..4b95c485 100644 --- a/src/main/java/com/skyflow/generated/rest/types/Locations.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/Locations.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java similarity index 86% rename from src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java index 9f019b66..40e73b99 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ProtobufAny.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java similarity index 93% rename from src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java index b5a4f6f5..a12660cb 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifiedFileOutput.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java index 024200d1..b7bc22ca 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ReidentifyFileResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java similarity index 89% rename from src/main/java/com/skyflow/generated/rest/types/ShiftDates.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java index 2fc4b11a..37ef8cdf 100644 --- a/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/ShiftDates.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ShiftDates.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java similarity index 94% rename from src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java index 81c1e58d..78df8838 100644 --- a/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/StringResponseEntities.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java index f8c1edb4..b703895d 100644 --- a/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/TokenTypeMapping.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = TokenTypeMapping.Builder.class) diff --git a/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 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/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 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/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 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/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 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/src/main/java/com/skyflow/generated/rest/types/Transformations.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/Transformations.java similarity index 87% rename from src/main/java/com/skyflow/generated/rest/types/Transformations.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/Transformations.java index 12a5afc7..c9165409 100644 --- a/src/main/java/com/skyflow/generated/rest/types/Transformations.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/Transformations.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java index c1ddbd58..8f5fea5a 100644 --- a/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/UploadFileV2Response.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java index d942fdcd..037b80bf 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditAfterOptions.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java similarity index 93% rename from src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java index c582d06d..f7ca73f0 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditEventResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java similarity index 86% rename from src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java index dd23d77c..36d7ccce 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1AuditResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java similarity index 93% rename from src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java index ca2bfdc2..a54eec6c 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEvent.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1AuditResponseEvent.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java similarity index 95% rename from src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java index 046c8fb4..90d9c538 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1AuditResponseEventRequest.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1AuditResponseEventRequest.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java similarity index 89% rename from src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java index a557da82..8f5fb775 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchOperationResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1BatchOperationResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java similarity index 97% rename from src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java index afc572cf..3f0455cc 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BatchRecord.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java similarity index 83% rename from src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java index 58472a8b..88c1c125 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BinListResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1BinListResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java similarity index 85% rename from src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java index ddb0d213..b7151598 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkDeleteRecordResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1BulkDeleteRecordResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java similarity index 83% rename from src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java index c2d3790a..27878fab 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1BulkGetRecordResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1BulkGetRecordResponse.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/V1Card.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1Card.java similarity index 96% rename from src/main/java/com/skyflow/generated/rest/types/V1Card.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1Card.java index f2a9db90..ba4f7349 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1Card.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1Card.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java index 4ec85903..4d9b24eb 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteFileResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java index 2f3bde14..5351d897 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DeleteRecordResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java index 3cf5b59c..b6fb9cb6 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordRequest.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java similarity index 93% rename from src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java index ba0abbfa..dd3a9c1f 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeRecordResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java similarity index 84% rename from src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java index 74f4f6e6..53660cbe 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1DetokenizeResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1DetokenizeResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java similarity index 91% rename from src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java index dfbccb8b..b64b8792 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1FieldRecords.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java index f5669085..43a86ae0 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetAuthTokenResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java similarity index 87% rename from src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java index 4bd67013..3138c766 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetFileScanStatusResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java similarity index 83% rename from src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java index 821d1f06..a44b0f62 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1GetQueryResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1GetQueryResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java similarity index 84% rename from src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java index 0addf786..f8a00001 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1InsertRecordResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1InsertRecordResponse.Builder.class) diff --git a/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 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/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java index f69719cf..17f77df6 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1RecordMetaProperties.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java index 2c15ceac..43fe264a 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordRequest.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java similarity index 87% rename from src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java index 201a4f60..66721fa1 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeRecordResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java similarity index 84% rename from src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java index 62d77bed..05e64bc0 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1TokenizeResponse.java @@ -3,20 +3,11 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; + +import java.util.*; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = V1TokenizeResponse.Builder.class) diff --git a/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java index 03ed7b58..d2fcbf1b 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1UpdateRecordResponse.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java similarity index 92% rename from src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java index c19d1cb5..6fc09854 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultFieldMapping.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java similarity index 91% rename from src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java index cd838305..1542ab0f 100644 --- a/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/V1VaultSchemaConfig.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java b/skyvault/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java similarity index 90% rename from src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java rename to skyvault/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java index 594b6d11..314b5942 100644 --- a/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java +++ b/skyvault/src/main/java/com/skyflow/generated/rest/types/WordCharacterCount.java @@ -3,15 +3,10 @@ */ package com.skyflow.generated.rest.types; -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.skyflow.generated.rest.core.ObjectMappers; + import java.util.HashMap; import java.util.Map; import java.util.Objects; diff --git a/skyvault/src/main/java/com/skyflow/utils/Constants.java b/skyvault/src/main/java/com/skyflow/utils/Constants.java new file mode 100644 index 00000000..2ca105d5 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/utils/Constants.java @@ -0,0 +1,42 @@ +package com.skyflow.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public final class Constants extends BaseConstants { + public static final String SDK_NAME = "Skyflow Java SDK"; + public static final String DEFAULT_SDK_VERSION = "v2"; + public static final String SDK_VERSION; + public static final String SDK_PREFIX; + public static final String SDK_METRIC_NAME_VERSION_PREFIX = "skyflow-java@"; + public static final String PROCESSED_FILE_NAME_PREFIX = "processed-"; + public static final String DEIDENTIFIED_FILE_PREFIX = "deidentified"; + public static final String HTTPS_PROTOCOL = "https"; + public static final String CURLY_PLACEHOLDER = "{%s}"; + public static final String EMPTY_STRING = ""; + public static final String QUOTE = "\""; + + public static final class HttpUtilityExtra { + public static final String SDK_GENERATED_PREFIX = "SDK-Generated-"; + private HttpUtilityExtra() {} + } + + static { + String sdkVersion; + // Use a static initializer block to read the properties file + Properties properties = new Properties(); + try (InputStream input = Constants.class.getClassLoader().getResourceAsStream("sdk.properties")) { + if (input == null) { + sdkVersion = DEFAULT_SDK_VERSION; + } else { + properties.load(input); + sdkVersion = properties.getProperty("sdk.version", DEFAULT_SDK_VERSION); + } + } catch (IOException ex) { + sdkVersion = DEFAULT_SDK_VERSION; + } + SDK_VERSION = sdkVersion; + SDK_PREFIX = SDK_NAME + " " + SDK_VERSION; + } +} diff --git a/src/main/java/com/skyflow/utils/HttpUtility.java b/skyvault/src/main/java/com/skyflow/utils/HttpUtility.java similarity index 100% rename from src/main/java/com/skyflow/utils/HttpUtility.java rename to skyvault/src/main/java/com/skyflow/utils/HttpUtility.java diff --git a/skyvault/src/main/java/com/skyflow/utils/Utils.java b/skyvault/src/main/java/com/skyflow/utils/Utils.java new file mode 100644 index 00000000..b5f81769 --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/utils/Utils.java @@ -0,0 +1,55 @@ +package com.skyflow.utils; + +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.enums.Env; +import com.skyflow.vault.connection.InvokeConnectionRequest; + +import java.util.HashMap; +import java.util.Map; + +public final class Utils extends BaseUtils { + public static String getVaultURL(String clusterId, Env env) { + return getVaultURL(clusterId, env, BaseConstants.V2_VAULT_DOMAIN); + } + + public static String constructConnectionURL(ConnectionConfig config, InvokeConnectionRequest invokeConnectionRequest) { + StringBuilder filledURL = new StringBuilder(config.getConnectionUrl()); + + if (invokeConnectionRequest.getPathParams() != null && !invokeConnectionRequest.getPathParams().isEmpty()) { + for (Map.Entry entry : invokeConnectionRequest.getPathParams().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value)); + } + } + + if (invokeConnectionRequest.getQueryParams() != null && !invokeConnectionRequest.getQueryParams().isEmpty()) { + filledURL.append("?"); + for (Map.Entry entry : invokeConnectionRequest.getQueryParams().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + filledURL.append(key).append("=").append(value).append("&"); + } + filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1)); + } + + return filledURL.toString(); + } + + public static Map constructConnectionHeadersMap(Map requestHeaders) { + Map headersMap = new HashMap<>(); + for (Map.Entry entry : requestHeaders.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + headersMap.put(key.toLowerCase(), value); + } + return headersMap; + } + + public static JsonObject getMetrics() { + JsonObject details = getCommonMetrics(); + details.addProperty(Constants.SDK_METRIC_NAME_VERSION, Constants.SDK_METRIC_NAME_VERSION_PREFIX + Constants.SDK_VERSION); + return details; + } +} diff --git a/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 src/main/java/com/skyflow/utils/logger/LogUtil.java rename to skyvault/src/main/java/com/skyflow/utils/logger/LogUtil.java diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/skyvault/src/main/java/com/skyflow/utils/validations/Validations.java similarity index 89% rename from src/main/java/com/skyflow/utils/validations/Validations.java rename to skyvault/src/main/java/com/skyflow/utils/validations/Validations.java index 5e528a6a..ff46c409 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/skyvault/src/main/java/com/skyflow/utils/validations/Validations.java @@ -6,11 +6,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.skyflow.config.BaseVaultConfig; import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; @@ -41,7 +40,7 @@ import com.skyflow.vault.tokens.DetokenizeRequest; import com.skyflow.vault.tokens.TokenizeRequest; -public class Validations { +public class Validations extends BaseValidations { private Validations() { } @@ -159,97 +158,6 @@ public static void validateInvokeConnectionRequest(InvokeConnectionRequest invok } } - public static void validateCredentials(Credentials credentials) throws SkyflowException { - int nonNullMembers = 0; - String path = credentials.getPath(); - String credentialsString = credentials.getCredentialsString(); - String token = credentials.getToken(); - String apiKey = credentials.getApiKey(); - Object context = credentials.getContext(); - ArrayList roles = credentials.getRoles(); - - if (path != null) nonNullMembers++; - if (credentialsString != null) nonNullMembers++; - if (token != null) nonNullMembers++; - if (apiKey != null) nonNullMembers++; - - if (nonNullMembers > 1) { - LogUtil.printErrorLog(ErrorLogs.MULTIPLE_TOKEN_GENERATION_MEANS_PASSED.getLog()); - throw new SkyflowException( - ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MultipleTokenGenerationMeansPassed.getMessage() - ); - } else if (nonNullMembers < 1) { - LogUtil.printErrorLog(ErrorLogs.NO_TOKEN_GENERATION_MEANS_PASSED.getLog()); - throw new SkyflowException( - ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.NoTokenGenerationMeansPassed.getMessage() - ); - } else if (path != null && path.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_PATH.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialFilePath.getMessage()); - } else if (credentialsString != null && credentialsString.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_CREDENTIALS_STRING.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyCredentialsString.getMessage()); - } else if (token != null && token.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_TOKEN_VALUE.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyToken.getMessage()); - } else if (apiKey != null) { - if (apiKey.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_API_KEY_VALUE.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyApikey.getMessage()); - } else { - Pattern pattern = Pattern.compile(Constants.API_KEY_REGEX); - Matcher matcher = pattern.matcher(apiKey); - if (!matcher.matches()) { - LogUtil.printErrorLog(ErrorLogs.INVALID_API_KEY.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidApikey.getMessage()); - } - } - } else if (roles != null) { - if (roles.isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_ROLES.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoles.getMessage()); - } else { - for (int index = 0; index < roles.size(); index++) { - String role = roles.get(index); - if (role == null || role.trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_ROLE_IN_ROLES.getLog(), Integer.toString(index) - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRoleInRoles.getMessage()); - } - } - } - } - if (context != null) { - if (context instanceof String) { - String ctxStr = (String) context; - if (ctxStr.trim().isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); - } - } else if (context instanceof Map) { - Map ctxMap = (Map) context; - if (ctxMap.isEmpty()) { - LogUtil.printErrorLog(ErrorLogs.EMPTY_OR_NULL_CONTEXT.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyContext.getMessage()); - } - Pattern ctxKeyPattern = Pattern.compile(Constants.CONTEXT_KEY_REGEX); - for (Object key : ctxMap.keySet()) { - if (key == null || !ctxKeyPattern.matcher(key.toString()).matches()) { - String keyStr = key == null ? "null" : key.toString(); - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.INVALID_CONTEXT_MAP_KEY.getLog(), keyStr)); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - Utils.parameterizedString(ErrorMessage.InvalidContextMapKey.getMessage(), keyStr)); - } - } - } else { - LogUtil.printErrorLog(ErrorLogs.INVALID_CONTEXT_TYPE.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidContextType.getMessage()); - } - } - } - public static void validateDetokenizeRequest(DetokenizeRequest detokenizeRequest) throws SkyflowException { ArrayList detokenizeData = detokenizeRequest.getDetokenizeData(); if (detokenizeData == null) { diff --git a/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 src/main/java/com/skyflow/vault/audit/ListEventRequest.java rename to skyvault/src/main/java/com/skyflow/vault/audit/ListEventRequest.java diff --git a/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 src/main/java/com/skyflow/vault/audit/ListEventResponse.java rename to skyvault/src/main/java/com/skyflow/vault/audit/ListEventResponse.java diff --git a/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 src/main/java/com/skyflow/vault/bin/GetBinRequest.java rename to skyvault/src/main/java/com/skyflow/vault/bin/GetBinRequest.java diff --git a/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 src/main/java/com/skyflow/vault/bin/GetBinResponse.java rename to skyvault/src/main/java/com/skyflow/vault/bin/GetBinResponse.java diff --git a/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 src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java rename to skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionRequest.java diff --git a/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 src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java rename to skyvault/src/main/java/com/skyflow/vault/connection/InvokeConnectionResponse.java diff --git a/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 src/main/java/com/skyflow/vault/controller/AuditController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/AuditController.java diff --git a/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 src/main/java/com/skyflow/vault/controller/BinLookupController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/BinLookupController.java diff --git a/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 src/main/java/com/skyflow/vault/controller/ConnectionController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/ConnectionController.java diff --git a/src/main/java/com/skyflow/vault/controller/DetectController.java b/skyvault/src/main/java/com/skyflow/vault/controller/DetectController.java similarity index 99% rename from src/main/java/com/skyflow/vault/controller/DetectController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/DetectController.java index 873c83e1..ad25d7e2 100644 --- a/src/main/java/com/skyflow/vault/controller/DetectController.java +++ b/skyvault/src/main/java/com/skyflow/vault/controller/DetectController.java @@ -8,12 +8,12 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.resources.files.requests.*; +import com.skyflow.generated.rest.types.*; import com.skyflow.generated.rest.core.ApiClientApiException; import com.skyflow.generated.rest.core.RequestOptions; -import com.skyflow.generated.rest.resources.files.requests.*; import com.skyflow.generated.rest.resources.strings.requests.DeidentifyStringRequest; import com.skyflow.generated.rest.resources.strings.requests.ReidentifyStringRequest; -import com.skyflow.generated.rest.types.*; import com.skyflow.logs.ErrorLogs; import com.skyflow.logs.InfoLogs; import com.skyflow.utils.Constants; @@ -352,7 +352,7 @@ private String extractBodyAsString(ApiClientApiException e) { private com.skyflow.generated.rest.types.DeidentifyFileResponse processFileByType(String fileExtension, String base64Content, DeidentifyFileRequest request, String vaultId) throws SkyflowException { switch (fileExtension.toLowerCase()) { case "txt": - com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText textFileRequest = + DeidentifyFileRequestDeidentifyText textFileRequest = super.getDeidentifyTextFileRequest(request, vaultId, base64Content); return super.getDetectFileAPi().deidentifyText(textFileRequest); @@ -424,7 +424,7 @@ public DeidentifyFileResponse getDetectRun(GetDetectRunRequest request) throws S .vaultId(vaultId) .build(); - com.skyflow.generated.rest.types.DetectRunsResponse apiResponse = + DetectRunsResponse apiResponse = super.getDetectFileAPi().getRun(runId, getRunRequest); return parseDeidentifyFileResponse(apiResponse, runId, apiResponse.getStatus().toString()); diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/skyvault/src/main/java/com/skyflow/vault/controller/VaultController.java similarity index 88% rename from src/main/java/com/skyflow/vault/controller/VaultController.java rename to skyvault/src/main/java/com/skyflow/vault/controller/VaultController.java index 1812b83b..1d82ac12 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/skyvault/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -17,6 +17,7 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.skyflow.VaultClient; +import com.skyflow.config.BaseVaultConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.RedactionType; @@ -73,10 +74,15 @@ import com.skyflow.vault.tokens.TokenizeRequest; import com.skyflow.vault.tokens.TokenizeResponse; -public final class VaultController extends VaultClient { +public final class VaultController extends VaultClient + implements IVaultController { private static final Gson GSON = new GsonBuilder().serializeNulls().create(); private static final JsonObject SKY_METADATA = Utils.getMetrics(); + private static RequestOptions buildRequestOptions() { + return RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + } + public VaultController(VaultConfig vaultConfig, Credentials credentials) { super(vaultConfig, credentials); } @@ -181,6 +187,7 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi return queryRecord; } + @Override public InsertResponse insert(InsertRequest insertRequest) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.INSERT_TRIGGERED.getLog()); V1InsertRecordResponse bulkInsertResult = null; @@ -194,7 +201,7 @@ public InsertResponse insert(InsertRequest insertRequest) throws SkyflowExceptio setBearerToken(); if (continueOnError) { RecordServiceBatchOperationBody insertBody = super.getBatchInsertRequestBody(insertRequest); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); batchInsertResult = super.getRecordsApi().withRawResponse().recordServiceBatchOperation(super.getVaultConfig().getVaultId(), insertBody, requestOptions); LogUtil.printInfoLog(InfoLogs.INSERT_REQUEST_RESOLVED.getLog()); Optional>> records = batchInsertResult.body().getResponses(); @@ -229,9 +236,7 @@ public InsertResponse insert(InsertRequest insertRequest) throws SkyflowExceptio } } } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.INSERT_RECORDS_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.INSERT_RECORDS_REJECTED); } LogUtil.printInfoLog(InfoLogs.INSERT_SUCCESS.getLog()); if (insertedFields.isEmpty()) { @@ -253,7 +258,7 @@ public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest) throws Validations.validateDetokenizeRequest(detokenizeRequest); setBearerToken(); V1DetokenizePayload payload = super.getDetokenizePayload(detokenizeRequest); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getTokensApi().withRawResponse().recordServiceDetokenize(super.getVaultConfig().getVaultId(), payload, requestOptions); LogUtil.printInfoLog(InfoLogs.DETOKENIZE_REQUEST_RESOLVED.getLog()); Map> responseHeaders = result.headers(); @@ -274,9 +279,7 @@ public DetokenizeResponse detokenize(DetokenizeRequest detokenizeRequest) throws } } } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.DETOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.DETOKENIZE_REQUEST_REJECTED); } if (!errorRecords.isEmpty()) { @@ -316,7 +319,7 @@ public GetResponse get(GetRequest getRequest) throws SkyflowException { .orderBy(RecordServiceBulkGetRecordRequestOrderBy.valueOf(getRequest.getOrderBy())) .build(); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getRecordsApi().recordServiceBulkGetRecord( super.getVaultConfig().getVaultId(), getRequest.getTable(), @@ -331,9 +334,7 @@ public GetResponse get(GetRequest getRequest) throws SkyflowException { } } } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.GET_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.GET_REQUEST_REJECTED); } LogUtil.printInfoLog(InfoLogs.GET_SUCCESS.getLog()); return new GetResponse(data, null); @@ -349,7 +350,7 @@ public UpdateResponse update(UpdateRequest updateRequest) throws SkyflowExceptio Validations.validateUpdateRequest(updateRequest); setBearerToken(); RecordServiceUpdateRecordBody updateBody = super.getUpdateRequestBody(updateRequest); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getRecordsApi().recordServiceUpdateRecord( super.getVaultConfig().getVaultId(), updateRequest.getTable(), @@ -361,9 +362,7 @@ public UpdateResponse update(UpdateRequest updateRequest) throws SkyflowExceptio skyflowId = String.valueOf(result.getSkyflowId()); tokensMap = getFormattedUpdateRecord(result); } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.UPDATE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.UPDATE_REQUEST_REJECTED); } LogUtil.printInfoLog(InfoLogs.UPDATE_SUCCESS.getLog()); return new UpdateResponse(skyflowId, tokensMap); @@ -379,14 +378,12 @@ public DeleteResponse delete(DeleteRequest deleteRequest) throws SkyflowExceptio RecordServiceBulkDeleteRecordBody deleteBody = RecordServiceBulkDeleteRecordBody.builder().skyflowIds(deleteRequest.getIds()) .build(); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getRecordsApi().recordServiceBulkDeleteRecord( super.getVaultConfig().getVaultId(), deleteRequest.getTable(), deleteBody, requestOptions); LogUtil.printInfoLog(InfoLogs.DELETE_REQUEST_RESOLVED.getLog()); } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.DELETE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.DELETE_REQUEST_REJECTED); } LogUtil.printInfoLog(InfoLogs.DELETE_SUCCESS.getLog()); return new DeleteResponse(result.getRecordIdResponse().orElse(Collections.emptyList())); @@ -400,7 +397,7 @@ public QueryResponse query(QueryRequest queryRequest) throws SkyflowException { LogUtil.printInfoLog(InfoLogs.VALIDATING_QUERY_REQUEST.getLog()); Validations.validateQueryRequest(queryRequest); setBearerToken(); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getQueryApi().queryServiceExecuteQuery( super.getVaultConfig().getVaultId(), QueryServiceExecuteQueryBody.builder().query(queryRequest.getQuery()).build(), @@ -414,9 +411,7 @@ public QueryResponse query(QueryRequest queryRequest) throws SkyflowException { } } } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.QUERY_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.QUERY_REQUEST_REJECTED); } LogUtil.printInfoLog(InfoLogs.QUERY_SUCCESS.getLog()); return new QueryResponse(fields); @@ -431,7 +426,7 @@ public TokenizeResponse tokenize(TokenizeRequest tokenizeRequest) throws Skyflow Validations.validateTokenizeRequest(tokenizeRequest); setBearerToken(); V1TokenizePayload payload = super.getTokenizePayload(tokenizeRequest); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); result = super.getTokensApi().recordServiceTokenize(super.getVaultConfig().getVaultId(), payload, requestOptions); LogUtil.printInfoLog(InfoLogs.TOKENIZE_REQUEST_RESOLVED.getLog()); if (result != null && result.getRecords().isPresent() && !result.getRecords().get().isEmpty()) { @@ -442,9 +437,7 @@ public TokenizeResponse tokenize(TokenizeRequest tokenizeRequest) throws Skyflow } } } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.TOKENIZE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.TOKENIZE_REQUEST_REJECTED); } LogUtil.printInfoLog(InfoLogs.TOKENIZE_SUCCESS.getLog()); return new TokenizeResponse(list); @@ -467,7 +460,7 @@ public FileUploadResponse uploadFile(FileUploadRequest fileUploadRequest) throws .returnFileMetadata(false) .build(); - RequestOptions requestOptions = RequestOptions.builder().addHeader(Constants.SDK_METRICS_HEADER_KEY, SKY_METADATA.toString()).build(); + RequestOptions requestOptions = buildRequestOptions(); UploadFileV2Response uploadFileV2Response = super.getRecordsApi().uploadFileV2( super.getVaultConfig().getVaultId(), file, @@ -481,9 +474,7 @@ public FileUploadResponse uploadFile(FileUploadRequest fileUploadRequest) throws ); } catch (ApiClientApiException e) { - String bodyString = GSON.toJson(e.body()); - LogUtil.printErrorLog(ErrorLogs.UPLOAD_FILE_REQUEST_REJECTED.getLog()); - throw new SkyflowException(e.statusCode(), e, e.headers(), bodyString); + throw wrapApiException(e.statusCode(), e, e.headers(), e.body(), ErrorLogs.UPLOAD_FILE_REQUEST_REJECTED); } catch (IOException e) { LogUtil.printErrorLog(ErrorLogs.UPLOAD_FILE_REQUEST_REJECTED.getLog()); throw new SkyflowException(e.getMessage(), e); diff --git a/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 src/main/java/com/skyflow/vault/data/DeleteRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/DeleteRequest.java diff --git a/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 src/main/java/com/skyflow/vault/data/DeleteResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/DeleteResponse.java diff --git a/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 src/main/java/com/skyflow/vault/data/FileUploadRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/FileUploadRequest.java diff --git a/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 src/main/java/com/skyflow/vault/data/FileUploadResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/FileUploadResponse.java diff --git a/src/main/java/com/skyflow/vault/data/GetRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/GetRequest.java similarity index 87% rename from src/main/java/com/skyflow/vault/data/GetRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/GetRequest.java index 0fccf7b8..edd9872e 100644 --- a/src/main/java/com/skyflow/vault/data/GetRequest.java +++ b/skyvault/src/main/java/com/skyflow/vault/data/GetRequest.java @@ -7,10 +7,11 @@ import java.util.ArrayList; -public class GetRequest { +public class GetRequest extends BaseGetRequest { private final GetRequestBuilder builder; private GetRequest(GetRequestBuilder builder) { + super(builder); this.builder = builder; } @@ -18,14 +19,6 @@ public static GetRequestBuilder builder() { return new GetRequestBuilder(); } - public String getTable() { - return this.builder.table; - } - - public ArrayList getIds() { - return this.builder.ids; - } - public RedactionType getRedactionType() { return this.builder.redactionType; } @@ -34,10 +27,6 @@ public Boolean getReturnTokens() { return this.builder.returnTokens; } - public ArrayList getFields() { - return this.builder.fields; - } - public String getOffset() { return this.builder.offset; } @@ -71,12 +60,9 @@ public String getOrderBy() { return this.builder.orderBy; } - public static final class GetRequestBuilder { - private String table; - private ArrayList ids; + public static final class GetRequestBuilder extends BaseGetRequestBuilder { private RedactionType redactionType; private Boolean returnTokens; - private ArrayList fields; private String offset; private String limit; private Boolean downloadUrl; @@ -89,13 +75,21 @@ private GetRequestBuilder() { this.downloadUrl = true; } + @Override public GetRequestBuilder table(String table) { - this.table = table; + super.table(table); return this; } + @Override public GetRequestBuilder ids(ArrayList ids) { - this.ids = ids; + super.ids(ids); + return this; + } + + @Override + public GetRequestBuilder fields(ArrayList fields) { + super.fields(fields); return this; } @@ -109,11 +103,6 @@ public GetRequestBuilder returnTokens(Boolean returnTokens) { return this; } - public GetRequestBuilder fields(ArrayList fields) { - this.fields = fields; - return this; - } - public GetRequestBuilder offset(String offset) { this.offset = offset; return this; diff --git a/skyvault/src/main/java/com/skyflow/vault/data/GetResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/GetResponse.java new file mode 100644 index 00000000..f9f082cd --- /dev/null +++ b/skyvault/src/main/java/com/skyflow/vault/data/GetResponse.java @@ -0,0 +1,15 @@ +package com.skyflow.vault.data; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Deprecation notice: the {@code skyflow_id} key in each {@link #getData()} record map is + * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility. + */ +public class GetResponse extends BaseGetResponse { + public GetResponse(ArrayList> data, ArrayList> errors) { + super(data, errors); + } +} diff --git a/src/main/java/com/skyflow/vault/data/InsertRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/InsertRequest.java similarity index 98% rename from src/main/java/com/skyflow/vault/data/InsertRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/InsertRequest.java index 54093467..a7066236 100644 --- a/src/main/java/com/skyflow/vault/data/InsertRequest.java +++ b/skyvault/src/main/java/com/skyflow/vault/data/InsertRequest.java @@ -5,13 +5,14 @@ import java.util.ArrayList; import java.util.HashMap; -public class InsertRequest { +public class InsertRequest extends BaseInsertRequest{ private final InsertRequestBuilder builder; private InsertRequest(InsertRequestBuilder builder) { this.builder = builder; } + public static InsertRequestBuilder builder() { return new InsertRequestBuilder(); } diff --git a/src/main/java/com/skyflow/vault/data/InsertResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/InsertResponse.java similarity index 93% rename from src/main/java/com/skyflow/vault/data/InsertResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/InsertResponse.java index 3d311525..29954b82 100644 --- a/src/main/java/com/skyflow/vault/data/InsertResponse.java +++ b/skyvault/src/main/java/com/skyflow/vault/data/InsertResponse.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.HashMap; -public class InsertResponse { +public class InsertResponse extends BaseInsertResponse { private final ArrayList> insertedFields; private final ArrayList> errors; diff --git a/src/main/java/com/skyflow/vault/data/QueryRequest.java b/skyvault/src/main/java/com/skyflow/vault/data/QueryRequest.java similarity index 59% rename from src/main/java/com/skyflow/vault/data/QueryRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/QueryRequest.java index 2aaa200d..ea27f5d1 100644 --- a/src/main/java/com/skyflow/vault/data/QueryRequest.java +++ b/skyvault/src/main/java/com/skyflow/vault/data/QueryRequest.java @@ -1,28 +1,21 @@ package com.skyflow.vault.data; -public class QueryRequest { - private final QueryRequestBuilder builder; - +public class QueryRequest extends BaseQueryRequest { private QueryRequest(QueryRequestBuilder builder) { - this.builder = builder; + super(builder); } public static QueryRequestBuilder builder() { return new QueryRequestBuilder(); } - public String getQuery() { - return this.builder.query; - } - - public static final class QueryRequestBuilder { - private String query; - + public static final class QueryRequestBuilder extends BaseQueryRequestBuilder { private QueryRequestBuilder() { } + @Override public QueryRequestBuilder query(String query) { - this.query = query; + super.query(query); return this; } diff --git a/src/main/java/com/skyflow/vault/data/QueryResponse.java b/skyvault/src/main/java/com/skyflow/vault/data/QueryResponse.java similarity index 52% rename from src/main/java/com/skyflow/vault/data/QueryResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/QueryResponse.java index afb32c60..4b51fc44 100644 --- a/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ b/skyvault/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -9,32 +9,14 @@ import java.util.ArrayList; import java.util.HashMap; -public class QueryResponse { - private final ArrayList> fields; - private final ArrayList> errors; - +/** + * Deprecation notice: the {@code skyflow_id} key in each {@link #getFields()} record map is + * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility. + */ +public class QueryResponse extends BaseQueryResponse { public QueryResponse(ArrayList> fields) { - this.fields = fields; - this.errors = null; - } - - /** - * Returns the list of record maps from the Query response. Each map contains all - * field name/value pairs for the record. - * - *

Deprecation notice: The {@code skyflow_id} key in each record map is - * deprecated and will be removed in an upcoming release. Use {@code skyflowId} instead. - * Both keys are present simultaneously in v2 for backward compatibility.

- */ - public ArrayList> getFields() { - return fields; - } - - /** - * Always returns null. The Query API does not support partial-error responses. - */ - public ArrayList> getErrors() { - return errors; + super(fields); } @Override diff --git a/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 src/main/java/com/skyflow/vault/data/UpdateRequest.java rename to skyvault/src/main/java/com/skyflow/vault/data/UpdateRequest.java diff --git a/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 src/main/java/com/skyflow/vault/data/UpdateResponse.java rename to skyvault/src/main/java/com/skyflow/vault/data/UpdateResponse.java diff --git a/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 src/main/java/com/skyflow/vault/detect/AudioBleep.java rename to skyvault/src/main/java/com/skyflow/vault/detect/AudioBleep.java diff --git a/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 src/main/java/com/skyflow/vault/detect/DateTransformation.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DateTransformation.java diff --git a/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 src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileRequest.java diff --git a/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 src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyFileResponse.java diff --git a/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 src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextRequest.java diff --git a/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 src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/DeidentifyTextResponse.java diff --git a/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 src/main/java/com/skyflow/vault/detect/EntityInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/EntityInfo.java diff --git a/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 src/main/java/com/skyflow/vault/detect/FileEntityInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileEntityInfo.java diff --git a/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 src/main/java/com/skyflow/vault/detect/FileInfo.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileInfo.java diff --git a/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 src/main/java/com/skyflow/vault/detect/FileInput.java rename to skyvault/src/main/java/com/skyflow/vault/detect/FileInput.java diff --git a/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 src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/GetDetectRunRequest.java diff --git a/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 src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java rename to skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextRequest.java diff --git a/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 src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java rename to skyvault/src/main/java/com/skyflow/vault/detect/ReidentifyTextResponse.java diff --git a/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 src/main/java/com/skyflow/vault/detect/TextIndex.java rename to skyvault/src/main/java/com/skyflow/vault/detect/TextIndex.java diff --git a/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 src/main/java/com/skyflow/vault/detect/TokenFormat.java rename to skyvault/src/main/java/com/skyflow/vault/detect/TokenFormat.java diff --git a/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 src/main/java/com/skyflow/vault/detect/Transformations.java rename to skyvault/src/main/java/com/skyflow/vault/detect/Transformations.java diff --git a/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 src/main/java/com/skyflow/vault/tokens/ColumnValue.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/ColumnValue.java diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java similarity index 73% rename from src/main/java/com/skyflow/vault/tokens/DetokenizeData.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java index 0e1b6b63..0a8d7ed5 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java +++ b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeData.java @@ -1,8 +1,9 @@ package com.skyflow.vault.tokens; import com.skyflow.enums.RedactionType; +import com.skyflow.vault.data.BaseDetokenizeData; -public class DetokenizeData { +public class DetokenizeData extends BaseDetokenizeData { private final String token; private final RedactionType redactionType; @@ -13,7 +14,7 @@ public DetokenizeData(String token) { public DetokenizeData(String token, RedactionType redactionType) { this.token = token; - this.redactionType = redactionType == null ? RedactionType.DEFAULT : redactionType; + this.redactionType = redactionType == null ? RedactionType.DEFAULT : redactionType; } public String getToken() { diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java similarity index 73% rename from src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java index 7d2ae73c..a16b17dd 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java +++ b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRecordResponse.java @@ -2,44 +2,29 @@ import com.skyflow.generated.rest.types.V1DetokenizeRecordResponse; +import com.skyflow.vault.data.BaseDetokenizeRecordResponse; -public class DetokenizeRecordResponse { - private final String token; - private final String value; +public class DetokenizeRecordResponse extends BaseDetokenizeRecordResponse { private final String type; - private final String error; private final String requestId; + private final String value; public DetokenizeRecordResponse(V1DetokenizeRecordResponse record) { this(record, null); } public DetokenizeRecordResponse(V1DetokenizeRecordResponse record, String requestId) { - this.token = record.getToken().orElse(null); - + super(record.getToken().orElse(null), record.getError().orElse(null)); this.value = record.getValue() .filter(val -> val != null && !val.toString().isEmpty()) .orElse(null); - this.type = record.getValueType() .map(Enum::toString) .filter(val -> !"NONE".equals(val)) .orElse(null); - this.error = record.getError().orElse(null); - this.requestId = requestId; } - - - public String getError() { - return error; - } - - public String getToken() { - return token; - } - public String getValue() { return value; } diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java similarity index 95% rename from src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 481c0c16..83c7e017 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -1,11 +1,12 @@ package com.skyflow.vault.tokens; import com.skyflow.logs.InfoLogs; +import com.skyflow.vault.data.BaseDetokenizeRequest; import com.skyflow.utils.logger.LogUtil; import java.util.ArrayList; -public class DetokenizeRequest { +public class DetokenizeRequest extends BaseDetokenizeRequest { private final DetokenizeRequestBuilder builder; private DetokenizeRequest(DetokenizeRequestBuilder builder) { diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java similarity index 94% rename from src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java index bd1d7cde..e7f69c34 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java +++ b/skyvault/src/main/java/com/skyflow/vault/tokens/DetokenizeResponse.java @@ -4,10 +4,11 @@ import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.skyflow.vault.data.BaseDetokenizeResponse; import java.util.ArrayList; -public class DetokenizeResponse { +public class DetokenizeResponse extends BaseDetokenizeResponse { private final ArrayList detokenizedFields; private final ArrayList errors; diff --git a/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 src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeRequest.java diff --git a/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java b/skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java similarity index 85% rename from src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java rename to skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java index d8d8072b..c77787cf 100644 --- a/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java +++ b/skyvault/src/main/java/com/skyflow/vault/tokens/TokenizeResponse.java @@ -1,9 +1,13 @@ package com.skyflow.vault.tokens; -import com.google.gson.*; - import java.util.List; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + public class TokenizeResponse { private final List tokens; diff --git a/src/main/resources/sdk.properties b/skyvault/src/main/resources/sdk.properties similarity index 100% rename from src/main/resources/sdk.properties rename to skyvault/src/main/resources/sdk.properties diff --git a/src/test/java/com/skyflow/ConnectionClientDotenvTests.java b/skyvault/src/test/java/com/skyflow/ConnectionClientDotenvTests.java similarity index 100% rename from src/test/java/com/skyflow/ConnectionClientDotenvTests.java rename to skyvault/src/test/java/com/skyflow/ConnectionClientDotenvTests.java diff --git a/src/test/java/com/skyflow/ConnectionClientTests.java b/skyvault/src/test/java/com/skyflow/ConnectionClientTests.java similarity index 100% rename from src/test/java/com/skyflow/ConnectionClientTests.java rename to skyvault/src/test/java/com/skyflow/ConnectionClientTests.java diff --git a/skyvault/src/test/java/com/skyflow/RequestFidelityTests.java b/skyvault/src/test/java/com/skyflow/RequestFidelityTests.java new file mode 100644 index 00000000..0a63d89e --- /dev/null +++ b/skyvault/src/test/java/com/skyflow/RequestFidelityTests.java @@ -0,0 +1,1406 @@ +package com.skyflow; + +import com.google.gson.JsonArray; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.Credentials; +import com.skyflow.config.VaultConfig; +import com.skyflow.enums.Env; +import com.skyflow.enums.RedactionType; +import com.skyflow.enums.RequestMethod; +import com.skyflow.enums.TokenMode; +import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.resources.query.QueryClient; +import com.skyflow.generated.rest.resources.query.requests.QueryServiceExecuteQueryBody; +import com.skyflow.generated.rest.resources.records.RawRecordsClient; +import com.skyflow.generated.rest.resources.records.RecordsClient; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkDeleteRecordBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceBulkGetRecordRequest; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceInsertRecordBody; +import com.skyflow.generated.rest.resources.records.requests.RecordServiceUpdateRecordBody; +import com.skyflow.generated.rest.resources.records.requests.UploadFileV2Request; +import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestOrderBy; +import com.skyflow.generated.rest.resources.records.types.RecordServiceBulkGetRecordRequestRedaction; +import com.skyflow.generated.rest.resources.tokens.requests.V1DetokenizePayload; +import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; +import com.skyflow.generated.rest.types.BatchRecordMethod; +import com.skyflow.generated.rest.types.RedactionEnumRedaction; +import com.skyflow.generated.rest.types.UploadFileV2Response; +import com.skyflow.generated.rest.types.V1BatchOperationResponse; +import com.skyflow.generated.rest.types.V1BatchRecord; +import com.skyflow.generated.rest.types.V1BulkDeleteRecordResponse; +import com.skyflow.generated.rest.types.V1BulkGetRecordResponse; +import com.skyflow.generated.rest.types.V1Byot; +import com.skyflow.generated.rest.types.V1DetokenizeRecordRequest; +import com.skyflow.generated.rest.types.V1FieldRecords; +import com.skyflow.generated.rest.types.V1GetQueryResponse; +import com.skyflow.generated.rest.types.V1InsertRecordResponse; +import com.skyflow.generated.rest.types.V1TokenizeRecordRequest; +import com.skyflow.generated.rest.types.V1TokenizeResponse; +import com.skyflow.generated.rest.types.V1UpdateRecordResponse; +import com.skyflow.utils.HttpUtility; +import com.skyflow.utils.Utils; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import com.skyflow.vault.controller.ConnectionController; +import com.skyflow.vault.controller.VaultController; +import com.skyflow.vault.data.DeleteRequest; +import com.skyflow.vault.data.FileUploadRequest; +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.data.InsertRequest; +import com.skyflow.vault.data.QueryRequest; +import com.skyflow.vault.data.UpdateRequest; +import com.skyflow.vault.tokens.ColumnValue; +import com.skyflow.vault.tokens.DetokenizeData; +import com.skyflow.vault.tokens.DetokenizeRequest; +import com.skyflow.vault.tokens.TokenizeRequest; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.ProtocolException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * Request-fidelity tests for the v2 SDK. + * + *

Every test here builds a request the way an SDK user would, runs it through the real + * production mapping code ({@link VaultClient}'s request-body builders, {@link VaultController}'s + * inline builders, {@link Utils} / {@link HttpUtility} for connections) and asserts the exact value + * that lands on the outgoing generated REST request object. + * + *

Tests suffixed {@code _knownGap} pin behaviour that is a confirmed defect. They assert the + * CURRENT behaviour on purpose so that any future change to production code is caught here. + */ +public class RequestFidelityTests { + + private static final String VAULT_ID = "vault123"; + private static final String CLUSTER_ID = "cluster123"; + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; // gitleaks:allow + private static final String CONNECTION_URL = "https://conn.example.com/api/{resource}/details"; + + // Non-trivial values reused across tests. + private static final String NON_ASCII_NAME = "日本語 テスト"; + private static final String NON_ASCII_CITY = "北京市 朝阳区"; + private static final String MULTILINE_NOTE = "line one\nline two with spaces"; + + // ------------------------------------------------------------------ + // harness + // ------------------------------------------------------------------ + + private static VaultConfig testVaultConfig() { + VaultConfig config = new VaultConfig(); + config.setVaultId(VAULT_ID); + config.setClusterId(CLUSTER_ID); + config.setEnv(Env.DEV); + return config; + } + + private static Credentials apiKeyCredentials() { + Credentials credentials = new Credentials(); + credentials.setApiKey(API_KEY); + return credentials; + } + + /** + * VaultClient's request-body builders are {@code protected} and this test lives in the same + * {@code com.skyflow} package, so they can be exercised directly with no mocking at all. + */ + private static VaultClient newVaultClient() { + return new VaultClient(testVaultConfig(), apiKeyCredentials()); + } + + private static VaultController newControllerWithMockApi(ApiClient mockApiClient) throws Exception { + VaultController controller = new VaultController(testVaultConfig(), apiKeyCredentials()); + Field field = VaultClient.class.getDeclaredField("apiClient"); + field.setAccessible(true); + field.set(controller, mockApiClient); + return controller; + } + + private static Response buildOkHttpResponse() { + return new Response.Builder() + .request(new Request.Builder().url("https://dummy.example.com").build()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .header(com.skyflow.utils.Constants.REQUEST_ID_HEADER_KEY, "req-fidelity-1") + .build(); + } + + private static HashMap richRow() { + HashMap nested = new HashMap<>(); + nested.put("city", NON_ASCII_CITY); + nested.put("zip", "100000"); + + HashMap row = new HashMap<>(); + row.put("name", NON_ASCII_NAME); + row.put("notes", MULTILINE_NOTE); + row.put("age", 42); + row.put("balance", 3.14); + row.put("active", true); + row.put("address", nested); + return row; + } + + private static ArrayList> rows(HashMap... maps) { + return new ArrayList<>(Arrays.asList(maps)); + } + + private static ArrayList list(String... items) { + return new ArrayList<>(Arrays.asList(items)); + } + + // ================================================================== + // insert — bulk branch (continueOnError == false) + // ================================================================== + + @Test + public void testInsertBulk_everyBuilderValueReachesGeneratedBody() { + VaultClient client = newVaultClient(); + + HashMap row1 = richRow(); + HashMap row2 = new HashMap<>(); + row2.put("name", "second record"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(row1, row2)) + .returnTokens(true) + .upsert("email") + .tokenMode(TokenMode.ENABLE_STRICT) + .continueOnError(false) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + + Assert.assertTrue("tokenization must carry returnTokens", body.getTokenization().isPresent()); + Assert.assertTrue(body.getTokenization().get()); + Assert.assertEquals("email", body.getUpsert().get()); + Assert.assertEquals(V1Byot.ENABLE_STRICT, body.getByot().get()); + + List records = body.getRecords().get(); + Assert.assertEquals(2, records.size()); + + Map fields0 = records.get(0).getFields().get(); + Assert.assertEquals(NON_ASCII_NAME, fields0.get("name")); + Assert.assertEquals(MULTILINE_NOTE, fields0.get("notes")); + Assert.assertEquals(42, fields0.get("age")); + Assert.assertEquals(3.14, (Double) fields0.get("balance"), 0.0); + Assert.assertEquals(Boolean.TRUE, fields0.get("active")); + @SuppressWarnings("unchecked") + Map address = (Map) fields0.get("address"); + Assert.assertEquals(NON_ASCII_CITY, address.get("city")); + Assert.assertEquals("100000", address.get("zip")); + Assert.assertEquals("whole values map must be carried verbatim", row1, fields0); + + Assert.assertEquals("second record", records.get(1).getFields().get().get("name")); + Assert.assertFalse("no tokens supplied -> tokens absent", records.get(0).getTokens().isPresent()); + } + + @Test + public void testInsertBulk_homogeneousReachesGeneratedBody() { + VaultClient client = newVaultClient(); + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(richRow())) + .homogeneous(true) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + Assert.assertTrue(body.getHomogeneous().isPresent()); + Assert.assertTrue(body.getHomogeneous().get()); + } + + @Test + public void testInsertBulk_defaultsReachGeneratedBody() { + VaultClient client = newVaultClient(); + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(richRow())) + .build(); + + RecordServiceInsertRecordBody body = client.getBulkInsertRequestBody(request); + Assert.assertFalse("returnTokens defaults to false", body.getTokenization().get()); + Assert.assertEquals("tokenMode defaults to DISABLE -> byot DISABLE", V1Byot.DISABLE, body.getByot().get()); + Assert.assertFalse("upsert not set -> absent", body.getUpsert().isPresent()); + Assert.assertFalse("homogeneous not set -> absent", body.getHomogeneous().isPresent()); + } + + @Test + public void testInsertBulk_tokensPairPositionallyWithValues() { + VaultClient client = newVaultClient(); + + HashMap value0 = new HashMap<>(); + value0.put("card_number", "4111111111111111"); + HashMap value1 = new HashMap<>(); + value1.put("card_number", "5111111111111111"); + + HashMap token0 = new HashMap<>(); + token0.put("card_number", "token-for-row-0"); + HashMap token1 = new HashMap<>(); + token1.put("card_number", "token-for-row-1"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(value0, value1)) + .tokens(rows(token0, token1)) + .tokenMode(TokenMode.ENABLE_STRICT) + .build(); + + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + + Assert.assertEquals("4111111111111111", records.get(0).getFields().get().get("card_number")); + Assert.assertEquals("token-for-row-0", records.get(0).getTokens().get().get("card_number")); + Assert.assertEquals("5111111111111111", records.get(1).getFields().get().get("card_number")); + Assert.assertEquals("token-for-row-1", records.get(1).getTokens().get().get("card_number")); + } + + @Test + public void testInsertBulk_recordOrderIsPreserved() { + VaultClient client = newVaultClient(); + ArrayList> values = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + HashMap row = new HashMap<>(); + row.put("position", i); + values.add(row); + } + + InsertRequest request = InsertRequest.builder().table("cards").values(values).build(); + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + + Assert.assertEquals(5, records.size()); + for (int i = 0; i < 5; i++) { + Assert.assertEquals("record order must be preserved", i, records.get(i).getFields().get().get("position")); + } + } + + @Test + public void testInsertBulk_fewerTokensThanValuesLeavesTrailingRecordsWithoutTokens() { + VaultClient client = newVaultClient(); + + HashMap value0 = new HashMap<>(); + value0.put("card_number", "4111111111111111"); + HashMap value1 = new HashMap<>(); + value1.put("card_number", "5111111111111111"); + HashMap token0 = new HashMap<>(); + token0.put("card_number", "token-0"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(value0, value1)) + .tokens(rows(token0)) + .tokenMode(TokenMode.ENABLE) + .build(); + + List records = client.getBulkInsertRequestBody(request).getRecords().get(); + Assert.assertEquals(2, records.size()); + Assert.assertEquals("token-0", records.get(0).getTokens().get().get("card_number")); + Assert.assertFalse("record without a matching token entry gets no tokens", + records.get(1).getTokens().isPresent()); + } + + // ================================================================== + // insert — batch branch (continueOnError == true) + // ================================================================== + + @Test + public void testInsertBatch_everyBuilderValueReachesGeneratedBody() { + VaultClient client = newVaultClient(); + + HashMap row1 = richRow(); + HashMap row2 = new HashMap<>(); + row2.put("name", "second record"); + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(rows(row1, row2)) + .returnTokens(true) + .upsert("email") + .tokenMode(TokenMode.ENABLE) + .continueOnError(true) + .build(); + + RecordServiceBatchOperationBody body = client.getBatchInsertRequestBody(request); + + Assert.assertTrue("batch body always sends continueOnError=true", body.getContinueOnError().get()); + Assert.assertEquals(V1Byot.ENABLE, body.getByot().get()); + + List records = body.getRecords().get(); + Assert.assertEquals(2, records.size()); + + V1BatchRecord first = records.get(0); + Assert.assertEquals("table maps to per-record tableName", "cards", first.getTableName().get()); + Assert.assertEquals(BatchRecordMethod.POST, first.getMethod().get()); + Assert.assertEquals("email", first.getUpsert().get()); + Assert.assertTrue(first.getTokenization().get()); + Assert.assertEquals("whole values map must be carried verbatim", row1, first.getFields().get()); + Assert.assertEquals(NON_ASCII_NAME, first.getFields().get().get("name")); + + Assert.assertEquals("cards", records.get(1).getTableName().get()); + Assert.assertEquals("second record", records.get(1).getFields().get().get("name")); + } + + @Test + public void testInsertBatch_tokensPairPositionallyAndOrderIsPreserved() { + VaultClient client = newVaultClient(); + + ArrayList> values = new ArrayList<>(); + ArrayList> tokens = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + HashMap row = new HashMap<>(); + row.put("position", i); + values.add(row); + + HashMap token = new HashMap<>(); + token.put("position", "token-" + i); + tokens.add(token); + } + + InsertRequest request = InsertRequest.builder() + .table("cards") + .values(values) + .tokens(tokens) + .tokenMode(TokenMode.ENABLE_STRICT) + .continueOnError(true) + .build(); + + List records = client.getBatchInsertRequestBody(request).getRecords().get(); + Assert.assertEquals(4, records.size()); + for (int i = 0; i < 4; i++) { + Assert.assertEquals(i, records.get(i).getFields().get().get("position")); + Assert.assertEquals("token-" + i, records.get(i).getTokens().get().get("position")); + } + } + + /** + * KNOWN GAP: {@code homogeneous} is silently dropped when {@code continueOnError(true)} routes + * the insert down the batch path — the batch body has no field able to carry it. + */ + @Test + public void testInsertBatch_homogeneousIsDropped_knownGap() { + VaultClient client = newVaultClient(); + + InsertRequest bulkRequest = InsertRequest.builder() + .table("cards").values(rows(richRow())).homogeneous(true).continueOnError(false).build(); + RecordServiceInsertRecordBody bulkBody = client.getBulkInsertRequestBody(bulkRequest); + Assert.assertTrue("bulk branch DOES send homogeneous", bulkBody.getHomogeneous().get()); + Assert.assertTrue(bulkBody.toString().contains("homogeneous")); + + InsertRequest batchRequest = InsertRequest.builder() + .table("cards").values(rows(richRow())).homogeneous(true).continueOnError(true).build(); + RecordServiceBatchOperationBody batchBody = client.getBatchInsertRequestBody(batchRequest); + + for (Method method : RecordServiceBatchOperationBody.class.getMethods()) { + Assert.assertFalse("batch body has no homogeneous accessor", + method.getName().toLowerCase(Locale.ROOT).contains("homogeneous")); + } + for (Method method : V1BatchRecord.class.getMethods()) { + Assert.assertFalse("batch record has no homogeneous accessor", + method.getName().toLowerCase(Locale.ROOT).contains("homogeneous")); + } + Assert.assertTrue(batchBody.getAdditionalProperties().isEmpty()); + Assert.assertFalse("homogeneous never reaches the wire on the batch branch", + batchBody.toString().contains("homogeneous")); + } + + @Test + public void testInsert_continueOnErrorFalseUsesBulkEndpointWithTableAsPathParam() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceInsertRecord(anyString(), anyString(), any())) + .thenReturn(V1InsertRecordResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + InsertRequest request = InsertRequest.builder() + .table("cards").values(rows(richRow())).returnTokens(true).continueOnError(false).build(); + controller.insert(request); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceInsertRecordBody.class); + Mockito.verify(mockRecords).recordServiceInsertRecord(eq(VAULT_ID), eq("cards"), bodyCaptor.capture()); + Assert.assertTrue(bodyCaptor.getValue().getTokenization().get()); + Mockito.verify(mockRecords, Mockito.never()).withRawResponse(); + } + + @Test + public void testInsert_continueOnErrorTrueUsesBatchEndpoint() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + RawRecordsClient mockRawRecords = Mockito.mock(RawRecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.withRawResponse()).thenReturn(mockRawRecords); + when(mockRawRecords.recordServiceBatchOperation(anyString(), any(), any())) + .thenReturn(new com.skyflow.generated.rest.core.ApiClientHttpResponse<>( + V1BatchOperationResponse.builder().build(), buildOkHttpResponse())); + + VaultController controller = newControllerWithMockApi(mockApi); + InsertRequest request = InsertRequest.builder() + .table("cards").values(rows(richRow())).continueOnError(true).build(); + controller.insert(request); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceBatchOperationBody.class); + Mockito.verify(mockRawRecords).recordServiceBatchOperation(eq(VAULT_ID), bodyCaptor.capture(), any()); + Assert.assertTrue(bodyCaptor.getValue().getContinueOnError().get()); + Assert.assertEquals("cards", bodyCaptor.getValue().getRecords().get().get(0).getTableName().get()); + Mockito.verify(mockRecords, Mockito.never()).recordServiceInsertRecord(anyString(), anyString(), any()); + } + + // ================================================================== + // update + // ================================================================== + + @Test + public void testUpdate_everyBuilderValueReachesGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", NON_ASCII_NAME); + data.put("notes", MULTILINE_NOTE); + data.put("age", 42); + + HashMap tokens = new HashMap<>(); + tokens.put("name", "token-for-name"); + + UpdateRequest request = UpdateRequest.builder() + .table("cards") + .data(data) + .tokens(tokens) + .returnTokens(true) + .tokenMode(TokenMode.ENABLE) + .build(); + + controller.update(request); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + eq(VAULT_ID), tableCaptor.capture(), idCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + Assert.assertEquals("skyflowId becomes the path id", "sky-id-1", idCaptor.getValue()); + + RecordServiceUpdateRecordBody body = bodyCaptor.getValue(); + Assert.assertTrue(body.getTokenization().get()); + Assert.assertEquals(V1Byot.ENABLE, body.getByot().get()); + + Map fields = body.getRecord().get().getFields().get(); + Assert.assertFalse("skyflowId must be stripped from fields", fields.containsKey("skyflowId")); + Assert.assertEquals(NON_ASCII_NAME, fields.get("name")); + Assert.assertEquals(MULTILINE_NOTE, fields.get("notes")); + Assert.assertEquals(42, fields.get("age")); + Assert.assertEquals(3, fields.size()); + + Assert.assertEquals("token-for-name", body.getRecord().get().getTokens().get().get("name")); + } + + @Test + public void testUpdate_deprecatedSkyflowIdKeyBecomesPathIdAndIsStripped() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("snake-id").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflow_id", "snake-id"); + data.put("name", NON_ASCII_NAME); + + controller.update(UpdateRequest.builder().table("cards").data(data).build()); + + ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + anyString(), anyString(), idCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("snake-id", idCaptor.getValue()); + Map fields = bodyCaptor.getValue().getRecord().get().getFields().get(); + Assert.assertFalse(fields.containsKey("skyflow_id")); + Assert.assertEquals(NON_ASCII_NAME, fields.get("name")); + } + + @Test + public void testUpdate_defaultsReachGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", "plain"); + + controller.update(UpdateRequest.builder().table("cards").data(data).build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceUpdateRecordBody.class); + Mockito.verify(mockRecords).recordServiceUpdateRecord( + anyString(), anyString(), anyString(), bodyCaptor.capture(), any()); + + Assert.assertFalse("returnTokens defaults to false", bodyCaptor.getValue().getTokenization().get()); + Assert.assertEquals(V1Byot.DISABLE, bodyCaptor.getValue().getByot().get()); + Assert.assertFalse("no tokens supplied -> tokens absent", + bodyCaptor.getValue().getRecord().get().getTokens().isPresent()); + } + + /** + * KNOWN GAP: {@code update()} removes {@code skyflowId} from the caller's own data map, so the + * same request object cannot be reused — a second call fails validation. + */ + @Test + public void testUpdate_mutatesCallersDataMap_knownGap() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceUpdateRecord(anyString(), anyString(), anyString(), any(), any())) + .thenReturn(V1UpdateRecordResponse.builder().skyflowId("sky-id-1").build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + HashMap data = new HashMap<>(); + data.put("skyflowId", "sky-id-1"); + data.put("name", NON_ASCII_NAME); + + UpdateRequest request = UpdateRequest.builder().table("cards").data(data).build(); + controller.update(request); + + Assert.assertFalse("caller's map is mutated: skyflowId removed", data.containsKey("skyflowId")); + Assert.assertSame("request still hands back the same (now-mutated) map", data, request.getData()); + + try { + controller.update(request); + Assert.fail("second update with the same request object should fail"); + } catch (SkyflowException e) { + Assert.assertTrue("fails because skyflowId is gone from the caller's map: " + e.getMessage(), + e.getMessage().contains("'skyflow_id' is missing from the data payload")); + } + } + + // ================================================================== + // get + // ================================================================== + + private static VaultController mockGetController(RecordsClient mockRecords) throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceBulkGetRecord(anyString(), anyString(), any(), any())) + .thenReturn(V1BulkGetRecordResponse.builder() + .records(Collections.emptyList()) + .build()); + return newControllerWithMockApi(mockApi); + } + + @Test + public void testGet_idsFieldsRedactionAndPagingReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + GetRequest request = GetRequest.builder() + .table("cards") + .ids(list("id-1", "id-2", "id-3")) + .fields(list("name", "card_number")) + .redactionType(RedactionType.PLAIN_TEXT) + .offset("10") + .limit("25") + .downloadUrl(false) + .orderBy("DESCENDING") + .build(); + + controller.get(request); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + eq(VAULT_ID), tableCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + RecordServiceBulkGetRecordRequest generated = requestCaptor.getValue(); + Assert.assertEquals("ids map to skyflowIds, order preserved", + Arrays.asList("id-1", "id-2", "id-3"), generated.getSkyflowIds().get()); + Assert.assertEquals(Arrays.asList("name", "card_number"), generated.getFields().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestRedaction.PLAIN_TEXT, generated.getRedaction().get()); + Assert.assertEquals("10", generated.getOffset().get()); + Assert.assertEquals("25", generated.getLimit().get()); + Assert.assertFalse("downloadUrl(false) must reach downloadURL", generated.getDownloadUrl().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.DESCENDING, generated.getOrderBy().get()); + Assert.assertFalse(generated.getColumnName().isPresent()); + Assert.assertFalse(generated.getColumnValues().isPresent()); + } + + @Test + public void testGet_columnNameAndColumnValuesReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + GetRequest request = GetRequest.builder() + .table("cards") + .columnName("email") + .columnValues(list("a@example.com", NON_ASCII_NAME, "value with spaces")) + .redactionType(RedactionType.MASKED) + .build(); + + controller.get(request); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + RecordServiceBulkGetRecordRequest generated = requestCaptor.getValue(); + Assert.assertEquals("email", generated.getColumnName().get()); + Assert.assertEquals(Arrays.asList("a@example.com", NON_ASCII_NAME, "value with spaces"), + generated.getColumnValues().get()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestRedaction.MASKED, generated.getRedaction().get()); + Assert.assertFalse(generated.getSkyflowIds().isPresent()); + } + + @Test + public void testGet_returnTokensMapsToTokenization() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).returnTokens(true).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertTrue(requestCaptor.getValue().getTokenization().get()); + Assert.assertFalse("no redactionType -> redaction absent", requestCaptor.getValue().getRedaction().isPresent()); + } + + @Test + public void testGet_defaultOrderByAndDownloadUrlReachGeneratedRequest() throws Exception { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.ASCENDING, + requestCaptor.getValue().getOrderBy().get()); + Assert.assertTrue("downloadUrl defaults to true", requestCaptor.getValue().getDownloadUrl().get()); + } + + @Test + public void testGet_orderByAcceptsAllThreeGeneratedEnumNames() throws Exception { + for (String orderBy : new String[]{"ASCENDING", "DESCENDING", "NONE"}) { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).orderBy(orderBy).build()); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + Assert.assertEquals(RecordServiceBulkGetRecordRequestOrderBy.valueOf(orderBy), + requestCaptor.getValue().getOrderBy().get()); + } + } + + /** + * KNOWN GAP: {@code orderBy} accepts any String but is mapped with {@code Enum.valueOf}, so an + * unrecognised value escapes as a raw {@link IllegalArgumentException} instead of a + * {@link SkyflowException}. + */ + @Test + public void testGet_orderByInvalidValueThrowsRawIllegalArgumentException_knownGap() throws Exception { + for (String orderBy : new String[]{"DESC", "descending", "asc"}) { + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + try { + controller.get(GetRequest.builder().table("cards").ids(list("id-1")).orderBy(orderBy).build()); + Assert.fail("expected an exception for orderBy=" + orderBy); + } catch (SkyflowException e) { + Assert.fail("orderBy=" + orderBy + " should NOT surface as SkyflowException (known gap)"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().contains(orderBy)); + } + Mockito.verify(mockRecords, Mockito.never()) + .recordServiceBulkGetRecord(anyString(), anyString(), any(), any()); + } + } + + /** + * KNOWN GAP: {@code downloadUrl(null)} silently coerces to {@code true}, while + * {@code returnTokens(null)} keeps the null (and therefore disappears from the request). + */ + @Test + public void testGet_downloadUrlNullCoercesToTrueButReturnTokensNullDoesNot_knownGap() throws Exception { + GetRequest request = GetRequest.builder() + .table("cards") + .ids(list("id-1")) + .downloadUrl(null) + .returnTokens(null) + .build(); + + Assert.assertTrue("downloadUrl(null) coerces to true", request.getDownloadUrl()); + Assert.assertNull("returnTokens(null) stays null", request.getReturnTokens()); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockGetController(mockRecords); + controller.get(request); + + ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(RecordServiceBulkGetRecordRequest.class); + Mockito.verify(mockRecords).recordServiceBulkGetRecord( + anyString(), anyString(), requestCaptor.capture(), any()); + + Assert.assertTrue("downloadURL=true is sent even though the user passed null", + requestCaptor.getValue().getDownloadUrl().get()); + Assert.assertFalse("tokenization is omitted for a null returnTokens", + requestCaptor.getValue().getTokenization().isPresent()); + } + + // ================================================================== + // delete + // ================================================================== + + @Test + public void testDelete_tableAndIdsReachGeneratedRequest() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.recordServiceBulkDeleteRecord(anyString(), anyString(), any(), any())) + .thenReturn(V1BulkDeleteRecordResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.delete(DeleteRequest.builder() + .table("cards") + .ids(list("id-3", "id-1", "id-2")) + .build()); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(RecordServiceBulkDeleteRecordBody.class); + Mockito.verify(mockRecords).recordServiceBulkDeleteRecord( + eq(VAULT_ID), tableCaptor.capture(), bodyCaptor.capture(), any()); + + Assert.assertEquals("cards", tableCaptor.getValue()); + Assert.assertEquals("ids map to skyflowIds, order preserved", + Arrays.asList("id-3", "id-1", "id-2"), bodyCaptor.getValue().getSkyflowIds().get()); + } + + // ================================================================== + // query + // ================================================================== + + @Test + public void testQuery_queryStringReachesGeneratedBodyVerbatim() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + QueryClient mockQuery = Mockito.mock(QueryClient.class); + when(mockApi.query()).thenReturn(mockQuery); + when(mockQuery.queryServiceExecuteQuery(anyString(), any(), any())) + .thenReturn(V1GetQueryResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + + String sql = "SELECT name, \"card number\"\nFROM cards\n" + + "WHERE name = 'Smith''s' AND city = '" + NON_ASCII_CITY + "'\nLIMIT 10"; + controller.query(QueryRequest.builder().query(sql).build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(QueryServiceExecuteQueryBody.class); + Mockito.verify(mockQuery).queryServiceExecuteQuery(eq(VAULT_ID), bodyCaptor.capture(), any()); + + Assert.assertEquals("query with quotes and newlines must be sent unchanged", + sql, bodyCaptor.getValue().getQuery().get()); + } + + @Test + public void testQuery_simpleQueryReachesGeneratedBody() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + QueryClient mockQuery = Mockito.mock(QueryClient.class); + when(mockApi.query()).thenReturn(mockQuery); + when(mockQuery.queryServiceExecuteQuery(anyString(), any(), any())) + .thenReturn(V1GetQueryResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.query(QueryRequest.builder().query("SELECT * FROM cards LIMIT 1").build()); + + ArgumentCaptor bodyCaptor = + ArgumentCaptor.forClass(QueryServiceExecuteQueryBody.class); + Mockito.verify(mockQuery).queryServiceExecuteQuery(anyString(), bodyCaptor.capture(), any()); + Assert.assertEquals("SELECT * FROM cards LIMIT 1", bodyCaptor.getValue().getQuery().get()); + } + + // ================================================================== + // tokenize + // ================================================================== + + @Test + public void testTokenize_columnValuesReachPayloadInOrder() { + VaultClient client = newVaultClient(); + + List columnValues = Arrays.asList( + ColumnValue.builder().value("4111111111111111").columnGroup("cg_cards").build(), + ColumnValue.builder().value(NON_ASCII_NAME).columnGroup("cg_names").build(), + ColumnValue.builder().value("value with spaces").columnGroup("cg_misc").build()); + + V1TokenizePayload payload = client.getTokenizePayload( + TokenizeRequest.builder().values(columnValues).build()); + + List parameters = payload.getTokenizationParameters().get(); + Assert.assertEquals(3, parameters.size()); + Assert.assertEquals("4111111111111111", parameters.get(0).getValue().get()); + Assert.assertEquals("cg_cards", parameters.get(0).getColumnGroup().get()); + Assert.assertEquals(NON_ASCII_NAME, parameters.get(1).getValue().get()); + Assert.assertEquals("cg_names", parameters.get(1).getColumnGroup().get()); + Assert.assertEquals("value with spaces", parameters.get(2).getValue().get()); + Assert.assertEquals("cg_misc", parameters.get(2).getColumnGroup().get()); + } + + @Test + public void testTokenize_payloadReachesGeneratedApiCall() throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + com.skyflow.generated.rest.resources.tokens.TokensClient mockTokens = + Mockito.mock(com.skyflow.generated.rest.resources.tokens.TokensClient.class); + when(mockApi.tokens()).thenReturn(mockTokens); + when(mockTokens.recordServiceTokenize(anyString(), any(), any())) + .thenReturn(V1TokenizeResponse.builder().build()); + + VaultController controller = newControllerWithMockApi(mockApi); + controller.tokenize(TokenizeRequest.builder().values(Collections.singletonList( + ColumnValue.builder().value(NON_ASCII_NAME).columnGroup("cg_names").build())).build()); + + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(V1TokenizePayload.class); + Mockito.verify(mockTokens).recordServiceTokenize(eq(VAULT_ID), payloadCaptor.capture(), any()); + + V1TokenizeRecordRequest parameter = payloadCaptor.getValue().getTokenizationParameters().get().get(0); + Assert.assertEquals(NON_ASCII_NAME, parameter.getValue().get()); + Assert.assertEquals("cg_names", parameter.getColumnGroup().get()); + } + + // ================================================================== + // detokenize + // ================================================================== + + @Test + public void testDetokenize_eachTokenKeepsItsOwnRedactionType() { + VaultClient client = newVaultClient(); + + ArrayList detokenizeData = new ArrayList<>(Arrays.asList( + new DetokenizeData("tok-default", RedactionType.DEFAULT), + new DetokenizeData("tok-plain", RedactionType.PLAIN_TEXT), + new DetokenizeData("tok-masked", RedactionType.MASKED), + new DetokenizeData("tok-redacted", RedactionType.REDACTED))); + + V1DetokenizePayload payload = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()); + + List parameters = payload.getDetokenizationParameters().get(); + Assert.assertEquals(4, parameters.size()); + + Assert.assertEquals("tok-default", parameters.get(0).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(0).getRedaction().get()); + Assert.assertEquals("tok-plain", parameters.get(1).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.PLAIN_TEXT, parameters.get(1).getRedaction().get()); + Assert.assertEquals("tok-masked", parameters.get(2).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.MASKED, parameters.get(2).getRedaction().get()); + Assert.assertEquals("tok-redacted", parameters.get(3).getToken().get()); + Assert.assertEquals(RedactionEnumRedaction.REDACTED, parameters.get(3).getRedaction().get()); + } + + @Test + public void testDetokenize_tokenWithoutRedactionDefaultsToDefault() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = new ArrayList<>(Arrays.asList( + new DetokenizeData("tok-a"), + new DetokenizeData("tok-b", null))); + + V1DetokenizePayload payload = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()); + + List parameters = payload.getDetokenizationParameters().get(); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(0).getRedaction().get()); + Assert.assertEquals(RedactionEnumRedaction.DEFAULT, parameters.get(1).getRedaction().get()); + } + + @Test + public void testDetokenize_continueOnErrorAndDownloadUrlReachPayload() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = + new ArrayList<>(Collections.singletonList(new DetokenizeData("tok-a"))); + + V1DetokenizePayload payload = client.getDetokenizePayload(DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .continueOnError(true) + .downloadUrl(true) + .build()); + Assert.assertTrue(payload.getContinueOnError().get()); + Assert.assertTrue(payload.getDownloadUrl().get()); + + V1DetokenizePayload defaults = client.getDetokenizePayload(DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .build()); + Assert.assertFalse("continueOnError defaults to false", defaults.getContinueOnError().get()); + Assert.assertFalse("downloadUrl defaults to false", defaults.getDownloadUrl().get()); + } + + @Test + public void testDetokenize_tokenOrderIsPreserved() { + VaultClient client = newVaultClient(); + ArrayList detokenizeData = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + detokenizeData.add(new DetokenizeData("tok-" + i, RedactionType.MASKED)); + } + + List parameters = client.getDetokenizePayload( + DetokenizeRequest.builder().detokenizeData(detokenizeData).build()) + .getDetokenizationParameters().get(); + + for (int i = 0; i < 5; i++) { + Assert.assertEquals("tok-" + i, parameters.get(i).getToken().get()); + } + } + + // ================================================================== + // uploadFile + // ================================================================== + + private static VaultController mockUploadController(RecordsClient mockRecords) throws Exception { + ApiClient mockApi = Mockito.mock(ApiClient.class); + when(mockApi.records()).thenReturn(mockRecords); + when(mockRecords.uploadFileV2(anyString(), any(File.class), any(), any())) + .thenReturn(UploadFileV2Response.builder().build()); + return newControllerWithMockApi(mockApi); + } + + @Test + public void testUploadFile_metadataFieldsAndFilePathReachGeneratedRequest() throws Exception { + File tempFile = File.createTempFile("fidelity-upload", ".txt"); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), "hello".getBytes("UTF-8")); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .skyflowId("sky-id-9") + .filePath(tempFile.getAbsolutePath()) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + eq(VAULT_ID), fileCaptor.capture(), requestCaptor.capture(), any()); + + UploadFileV2Request generated = requestCaptor.getValue(); + Assert.assertEquals("table maps to tableName", "cards", generated.getTableName()); + Assert.assertEquals("file_column", generated.getColumnName()); + Assert.assertEquals("sky-id-9", generated.getSkyflowId().get()); + Assert.assertFalse("returnFileMetadata is hard-coded to false", generated.getReturnFileMetadata().get()); + Assert.assertEquals(tempFile.getAbsolutePath(), fileCaptor.getValue().getPath()); + } + + @Test + public void testUploadFile_base64AndFileNameProduceMatchingFile() throws Exception { + File tempDir = Files.createTempDirectory("fidelity-b64").toFile(); + tempDir.deleteOnExit(); + File target = new File(tempDir, "decoded-upload.txt"); + target.deleteOnExit(); + + byte[] payload = "café ☕".getBytes("UTF-8"); + String base64 = Base64.getEncoder().encodeToString(payload); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .skyflowId("sky-id-b64") + .base64(base64) + .fileName(target.getAbsolutePath()) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + anyString(), fileCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertEquals(target.getAbsolutePath(), fileCaptor.getValue().getPath()); + Assert.assertArrayEquals("base64 must be decoded into the named file", + payload, Files.readAllBytes(fileCaptor.getValue().toPath())); + Assert.assertEquals("sky-id-b64", requestCaptor.getValue().getSkyflowId().get()); + } + + @Test + public void testUploadFile_fileObjectIsPassedThroughUnchanged() throws Exception { + File tempFile = File.createTempFile("fidelity-object", ".bin"); + tempFile.deleteOnExit(); + Files.write(tempFile.toPath(), new byte[]{1, 2, 3}); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .fileObject(tempFile) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(UploadFileV2Request.class); + Mockito.verify(mockRecords).uploadFileV2( + anyString(), fileCaptor.capture(), requestCaptor.capture(), any()); + + Assert.assertSame("the caller's File instance must be forwarded", tempFile, fileCaptor.getValue()); + Assert.assertFalse("no skyflowId supplied -> absent", requestCaptor.getValue().getSkyflowId().isPresent()); + } + + @Test + public void testUploadFile_filePathTakesPrecedenceOverOtherSources() throws Exception { + File pathFile = File.createTempFile("fidelity-precedence-path", ".txt"); + pathFile.deleteOnExit(); + Files.write(pathFile.toPath(), "from-path".getBytes("UTF-8")); + + File objectFile = File.createTempFile("fidelity-precedence-object", ".txt"); + objectFile.deleteOnExit(); + Files.write(objectFile.toPath(), "from-object".getBytes("UTF-8")); + + RecordsClient mockRecords = Mockito.mock(RecordsClient.class); + VaultController controller = mockUploadController(mockRecords); + + controller.uploadFile(FileUploadRequest.builder() + .table("cards") + .columnName("file_column") + .filePath(pathFile.getAbsolutePath()) + .fileObject(objectFile) + .build()); + + ArgumentCaptor fileCaptor = ArgumentCaptor.forClass(File.class); + Mockito.verify(mockRecords).uploadFileV2(anyString(), fileCaptor.capture(), any(), any()); + Assert.assertEquals(pathFile.getAbsolutePath(), fileCaptor.getValue().getPath()); + } + + // ================================================================== + // invoke — URL construction (com.skyflow.utils.Utils) + // ================================================================== + + private static ConnectionConfig connectionConfig(String url) { + ConnectionConfig config = new ConnectionConfig(); + config.setConnectionId("conn123"); + config.setConnectionUrl(url); + config.setCredentials(apiKeyCredentials()); + return config; + } + + @Test + public void testInvoke_pathParamsAreSubstitutedIntoTheUrl() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/details", url); + } + + @Test + public void testInvoke_queryParamsAreAppendedInOrderWithNoTrailingAmpersand() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("limit", "10"); + queryParams.put("offset", "20"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("https://conn.example.com/api?limit=10&offset=20", url); + } + + /** + * KNOWN GAP: query-param values are concatenated raw, so a value containing {@code &} or + * {@code =} re-partitions the query string into extra parameters. + */ + @Test + public void testInvoke_queryParamValuesAreNotPercentEncoded_knownGap() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("q", "a&b=c"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("value is injected raw", "https://conn.example.com/api?q=a&b=c", url); + + String queryString = url.substring(url.indexOf('?') + 1); + Assert.assertEquals("one user param has become two wire params", 2, queryString.split("&").length); + Assert.assertEquals("q=a", queryString.split("&")[0]); + Assert.assertEquals("b=c", queryString.split("&")[1]); + } + + /** + * KNOWN GAP: a query-param value containing a space is injected raw, producing an invalid URI. + */ + @Test + public void testInvoke_queryParamValueWithSpaceIsNotEncoded_knownGap() { + Map queryParams = new LinkedHashMap<>(); + queryParams.put("name", "John Doe"); + + String url = Utils.constructConnectionURL(connectionConfig("https://conn.example.com/api"), + InvokeConnectionRequest.builder().queryParams(queryParams).build()); + + Assert.assertEquals("https://conn.example.com/api?name=John Doe", url); + Assert.assertFalse("no %20 encoding is applied", url.contains("%20")); + try { + java.net.URI.create(url); + Assert.fail("raw space should make this an invalid URI"); + } catch (IllegalArgumentException expected) { + Assert.assertTrue(expected.getMessage().contains("Illegal character")); + } + } + + /** + * KNOWN GAP: path-param values are substituted raw, so a value containing {@code /} silently + * changes the URL's path structure. + */ + @Test + public void testInvoke_pathParamValuesAreNotPercentEncoded_knownGap() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards/123"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/123/details", url); + Assert.assertFalse("no %2F encoding is applied", url.toUpperCase(Locale.ROOT).contains("%2F")); + Assert.assertEquals("the path gained a segment", 5, java.net.URI.create(url).getPath().split("/").length); + } + + /** + * KNOWN GAP: a path-param entry with no matching {@code {placeholder}} is discarded silently — + * no error, no log, and the URL is unchanged. + */ + @Test + public void testInvoke_pathParamWithoutMatchingPlaceholderIsSilentlyDiscarded_knownGap() { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + pathParams.put("thisKeyIsNotInTheUrl", "ignored-value"); + + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().pathParams(pathParams).build()); + + Assert.assertEquals("https://conn.example.com/api/cards/details", url); + Assert.assertFalse(url.contains("ignored-value")); + Assert.assertFalse(url.contains("thisKeyIsNotInTheUrl")); + } + + @Test + public void testInvoke_unfilledPlaceholderRemainsInTheUrl() { + String url = Utils.constructConnectionURL(connectionConfig(CONNECTION_URL), + InvokeConnectionRequest.builder().build()); + Assert.assertEquals("https://conn.example.com/api/{resource}/details", url); + } + + // ================================================================== + // invoke — header construction (com.skyflow.utils.Utils) + // ================================================================== + + @Test + public void testInvoke_headerKeysAreLowercasedAndValuesPreserved() { + Map headers = new LinkedHashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", NON_ASCII_NAME); + + Map constructed = Utils.constructConnectionHeadersMap(headers); + + Assert.assertEquals(2, constructed.size()); + Assert.assertEquals("application/json", constructed.get("content-type")); + Assert.assertEquals(NON_ASCII_NAME, constructed.get("x-custom-header")); + } + + /** + * KNOWN GAP: header keys are lowercased without a {@link Locale}, and two keys differing only + * in case collapse into a single entry (last writer wins). + */ + @Test + public void testInvoke_headerKeysDifferingOnlyInCaseCollapse_knownGap() { + Map headers = new LinkedHashMap<>(); + headers.put("X-Custom", "first-value"); + headers.put("x-custom", "second-value"); + headers.put("X-CUSTOM", "third-value"); + + Map constructed = Utils.constructConnectionHeadersMap(headers); + + Assert.assertEquals("three user headers collapse into one", 1, constructed.size()); + Assert.assertEquals("last writer wins", "third-value", constructed.get("x-custom")); + } + + // ================================================================== + // invoke — body encoding (com.skyflow.utils.HttpUtility) + // ================================================================== + + @Test + public void testInvoke_formEncodedBodyEncodesKeysValuesAndNestedObjects() { + JsonObject body = new JsonObject(); + body.addProperty("name", "John Doe"); + JsonObject nested = new JsonObject(); + nested.addProperty("city", "New York"); + body.add("address", nested); + + String encoded = HttpUtility.formatJsonToFormEncodedString(body); + + Assert.assertTrue(encoded.contains("name=John+Doe")); + Assert.assertTrue("nested objects are flattened to key[subkey]", + encoded.contains("address%5Bcity%5D=New+York")); + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, a 1-element JSON array silently + * loses its array-ness and is sent as a bare scalar. + */ + @Test + public void testInvoke_formEncodedSingleElementArrayLosesArrayness_knownGap() { + JsonObject body = new JsonObject(); + JsonArray items = new JsonArray(); + items.add("only-item"); + body.add("items", items); + + Assert.assertEquals("items=only-item", HttpUtility.formatJsonToFormEncodedString(body)); + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, an array of 2+ elements blows up + * with a raw {@link IllegalStateException}. + */ + @Test + public void testInvoke_formEncodedMultiElementArrayThrowsIllegalState_knownGap() { + JsonObject body = new JsonObject(); + JsonArray items = new JsonArray(); + items.add("first"); + items.add("second"); + body.add("items", items); + + try { + HttpUtility.formatJsonToFormEncodedString(body); + Assert.fail("expected IllegalStateException for a multi-element array"); + } catch (IllegalStateException expected) { + Assert.assertTrue(expected.getMessage().contains("2")); + } + } + + /** + * KNOWN GAP: with {@code application/x-www-form-urlencoded}, a JSON null blows up with a raw + * {@link UnsupportedOperationException}. + */ + @Test + public void testInvoke_formEncodedJsonNullThrowsUnsupportedOperation_knownGap() { + JsonObject body = new JsonObject(); + body.add("maybe", JsonNull.INSTANCE); + + try { + HttpUtility.formatJsonToFormEncodedString(body); + Assert.fail("expected UnsupportedOperationException for a JSON null"); + } catch (UnsupportedOperationException expected) { + Assert.assertEquals("JsonNull", expected.getMessage()); + } + } + + /** + * KNOWN GAP: the multipart encoder shares {@code convertJsonToMap} and therefore has exactly + * the same array/null defects. + */ + @Test + public void testInvoke_multipartBodyHasTheSameArrayAndNullDefects_knownGap() { + JsonObject single = new JsonObject(); + JsonArray oneItem = new JsonArray(); + oneItem.add("only-item"); + single.add("items", oneItem); + Assert.assertTrue("array-ness is lost", + HttpUtility.formatJsonToMultiPartFormDataString(single, "bnd").contains("only-item")); + + JsonObject many = new JsonObject(); + JsonArray twoItems = new JsonArray(); + twoItems.add("first"); + twoItems.add("second"); + many.add("items", twoItems); + try { + HttpUtility.formatJsonToMultiPartFormDataString(many, "bnd"); + Assert.fail("expected IllegalStateException for a multi-element array"); + } catch (IllegalStateException expected) { + Assert.assertNotNull(expected.getMessage()); + } + + JsonObject withNull = new JsonObject(); + withNull.add("maybe", JsonNull.INSTANCE); + try { + HttpUtility.formatJsonToMultiPartFormDataString(withNull, "bnd"); + Assert.fail("expected UnsupportedOperationException for a JSON null"); + } catch (UnsupportedOperationException expected) { + Assert.assertEquals("JsonNull", expected.getMessage()); + } + } + + // ================================================================== + // invoke — transport + // ================================================================== + + /** + * KNOWN GAP: {@link RequestMethod#PATCH} can be built into a request but never reaches the + * wire — {@code HttpURLConnection.setRequestMethod} rejects it, surfacing as a + * {@link SkyflowException} wrapping a {@link ProtocolException}. + */ + @Test + public void testInvoke_patchMethodCannotBeSent_knownGap() { + ConnectionController controller = + new ConnectionController(connectionConfig("https://conn.example.com/api"), apiKeyCredentials()); + + try { + controller.invoke(InvokeConnectionRequest.builder().method(RequestMethod.PATCH).build()); + Assert.fail("PATCH should not be sendable"); + } catch (SkyflowException e) { + Assert.assertTrue("cause must be a ProtocolException, got: " + e.getCause(), + e.getCause() instanceof ProtocolException); + Assert.assertTrue(e.getCause().getMessage().contains("PATCH")); + } + } +} diff --git a/src/test/java/com/skyflow/SkyflowTests.java b/skyvault/src/test/java/com/skyflow/SkyflowTests.java similarity index 62% rename from src/test/java/com/skyflow/SkyflowTests.java rename to skyvault/src/test/java/com/skyflow/SkyflowTests.java index 983419a7..07386a5b 100644 --- a/src/test/java/com/skyflow/SkyflowTests.java +++ b/skyvault/src/test/java/com/skyflow/SkyflowTests.java @@ -1,8 +1,9 @@ package com.skyflow; -import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.enums.LogLevel; import com.skyflow.errors.ErrorCode; @@ -102,55 +103,6 @@ public void testAddingValidVaultConfigInSkyflowClient() { } } - @Test - public void testAddingExistingVaultConfigInSkyflowClient() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.SANDBOX); - Skyflow skyflowClient = Skyflow.builder().build(); - skyflowClient.addVaultConfig(config).addVaultConfig(config); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdAlreadyInConfigList.getMessage(), e.getMessage()); - } - } - - @Test - public void testUpdatingNonExistentVaultConfigInSkyflowBuilder() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.SANDBOX); - Skyflow.builder().updateVaultConfig(config).build(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - - @Test - public void testUpdatingNonExistentVaultConfigInSkyflowClient() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.SANDBOX); - Skyflow skyflowClient = Skyflow.builder().build(); - skyflowClient.updateVaultConfig(config); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } catch (Exception e) { - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - @Test public void testUpdatingValidVaultConfigInSkyflowClient() { try { @@ -173,114 +125,6 @@ public void testUpdatingValidVaultConfigInSkyflowClient() { } } - @Test - public void testUpdateVaultConfigNullCredentialsFallsBackToPrevious() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.SANDBOX); - - Credentials creds = new Credentials(); - creds.setToken(token); - config.setCredentials(creds); - - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(config).build(); - - // Update with null credentials — should retain previous credentials value - VaultConfig partialUpdate = new VaultConfig(); - partialUpdate.setVaultId(vaultID); - partialUpdate.setClusterId(clusterID); - skyflowClient.updateVaultConfig(partialUpdate); - Assert.assertNotNull(skyflowClient.getVaultConfig(vaultID).getCredentials()); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testRemovingNonExistentVaultConfigInSkyflowBuilder() { - try { - Skyflow.builder().removeVaultConfig(vaultID).build(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - - @Test - public void testRemovingNonExistentVaultConfigInSkyflowClient() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.SANDBOX); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(config).build(); - skyflowClient.removeVaultConfig(vaultID); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testRemovingValidVaultConfigInSkyflowClient() { - try { - Skyflow skyflowClient = Skyflow.builder().build(); - skyflowClient.removeVaultConfig(vaultID); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - - @Test - public void testGettingNonExistentVaultConfigInSkyflowClient() { - try { - Skyflow skyflowClient = Skyflow.builder().build(); - VaultConfig config = skyflowClient.getVaultConfig(vaultID); - Assert.assertNull(config); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGettingAlreadyRemovedVaultFromEmptyConfigs() { - try { - Skyflow skyflowClient = Skyflow.builder().build(); - skyflowClient.vault(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - - @Test - public void testGettingAlreadyRemovedVaultFromNonEmptyConfigs() { - try { - VaultConfig primaryConfig = new VaultConfig(); - primaryConfig.setVaultId(vaultID); - primaryConfig.setClusterId(clusterID); - primaryConfig.setEnv(Env.SANDBOX); - - VaultConfig secondaryConfig = new VaultConfig(); - secondaryConfig.setVaultId(vaultID + "123"); - secondaryConfig.setClusterId(clusterID); - secondaryConfig.setEnv(Env.SANDBOX); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(primaryConfig).addVaultConfig(secondaryConfig).build(); - skyflowClient.removeVaultConfig(vaultID); - skyflowClient.vault(vaultID); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.VaultIdNotInConfigList.getMessage(), e.getMessage()); - } - } - - @Test public void testAddingInvalidConnectionConfigInSkyflowBuilder() { try { @@ -460,60 +304,6 @@ public void testGettingNonExistentConnectionConfigInSkyflowClient() { } } - @Test - public void testAddingInvalidSkyflowCredentialsInSkyflowBuilder() { - try { - Credentials credentials = new Credentials(); - Skyflow.builder().addSkyflowCredentials(credentials).build(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.NoTokenGenerationMeansPassed.getMessage(), e.getMessage()); - } - } - - @Test - public void testUpdatingValidSkyflowCredentialsInSkyflowClient() { - try { - VaultConfig vaultConfig = new VaultConfig(); - vaultConfig.setVaultId(vaultID); - vaultConfig.setClusterId(clusterID); - - ConnectionConfig connectionConfig = new ConnectionConfig(); - connectionConfig.setConnectionId(connectionID); - connectionConfig.setConnectionUrl(connectionURL); - - Credentials credentials = new Credentials(); - credentials.setToken(token); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(vaultConfig).addConnectionConfig(connectionConfig).build(); - skyflowClient.updateSkyflowCredentials(credentials); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testDefaultLogLevel() { - try { - Skyflow skyflowClient = Skyflow.builder().setLogLevel(null).build(); - Assert.assertEquals(LogLevel.ERROR, skyflowClient.getLogLevel()); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testSetLogLevel() { - try { - Skyflow skyflowClient = Skyflow.builder().setLogLevel(LogLevel.INFO).build(); - Assert.assertEquals(LogLevel.INFO, skyflowClient.getLogLevel()); - skyflowClient.setLogLevel(LogLevel.WARN); - Assert.assertEquals(LogLevel.WARN, skyflowClient.getLogLevel()); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - @Test @SuppressWarnings("deprecation") public void testUpdateLogLevel() { @@ -686,36 +476,6 @@ public void testDetectMethodWithInvalidVaultId() { } } - @Test - public void testUpdateVaultConfig_withNewClusterIdAndCredentials_updatesAllFields() { - try { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(Env.DEV); - Credentials creds = new Credentials(); - creds.setToken(token); - config.setCredentials(creds); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(config).build(); - - // Update with a new non-null clusterId and new non-null credentials — covers - // the non-null (true) branches for all three ternaries in findAndUpdateVaultConfig - Credentials newCreds = new Credentials(); - newCreds.setToken("updated-token-value"); - VaultConfig update = new VaultConfig(); - update.setVaultId(vaultID); - update.setClusterId(newClusterID); - update.setEnv(Env.PROD); - update.setCredentials(newCreds); - skyflowClient.updateVaultConfig(update); - Assert.assertEquals(newClusterID, skyflowClient.getVaultConfig(vaultID).getClusterId()); - Assert.assertEquals(Env.PROD, skyflowClient.getVaultConfig(vaultID).getEnv()); - Assert.assertEquals("updated-token-value", skyflowClient.getVaultConfig(vaultID).getCredentials().getToken()); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - @Test public void testUpdateConnectionConfig_withNewCredentials_updatesCredentials() { try { @@ -743,75 +503,6 @@ public void testUpdateConnectionConfig_withNewCredentials_updatesCredentials() { } } - @Test - public void testUpdateVaultConfig_withNullEnv_fallsBackToPreviousEnv() { - // VaultConfig's constructor defaults env=PROD so getEnv() is never null via normal API. - // Use an anonymous subclass to make getEnv() return null, exercising the false branch - // of `vaultConfig.getEnv() != null` in findAndUpdateVaultConfig. - try { - Credentials creds = new Credentials(); - creds.setToken(token); - VaultConfig initial = new VaultConfig(); - initial.setVaultId(vaultID); - initial.setClusterId(clusterID); - initial.setEnv(Env.SANDBOX); - initial.setCredentials(creds); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(initial).build(); - - VaultConfig updateWithNullEnv = new VaultConfig() { - @Override public Env getEnv() { return null; } - }; - updateWithNullEnv.setVaultId(vaultID); - updateWithNullEnv.setClusterId(clusterID); - updateWithNullEnv.setCredentials(creds); - - skyflowClient.updateVaultConfig(updateWithNullEnv); - // env falls back to previous (SANDBOX) - Assert.assertEquals(Env.SANDBOX, skyflowClient.getVaultConfig(vaultID).getEnv()); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testFindAndUpdateVaultConfig_withNullClusterId_fallsBackToPreviousClusterId() { - // Validation enforces non-null clusterId, so the false branch of - // `vaultConfig.getClusterId() != null` in findAndUpdateVaultConfig is unreachable - // via the normal flow. Call the private method directly via reflection. - try { - Credentials creds = new Credentials(); - creds.setToken(token); - VaultConfig initial = new VaultConfig(); - initial.setVaultId(vaultID); - initial.setClusterId(clusterID); - initial.setEnv(Env.DEV); - initial.setCredentials(creds); - Skyflow skyflowClient = Skyflow.builder().addVaultConfig(initial).build(); - - java.lang.reflect.Field builderField = Skyflow.class.getDeclaredField("builder"); - builderField.setAccessible(true); - Object builder = builderField.get(skyflowClient); - - VaultConfig nullClusterConfig = new VaultConfig(); - nullClusterConfig.setVaultId(vaultID); - // Override clusterId field to null via reflection (setter enforces non-null) - java.lang.reflect.Field clusterIdField = VaultConfig.class.getDeclaredField("clusterId"); - clusterIdField.setAccessible(true); - clusterIdField.set(nullClusterConfig, null); - - java.lang.reflect.Method method = builder.getClass().getDeclaredMethod( - "findAndUpdateVaultConfig", VaultConfig.class); - method.setAccessible(true); - VaultConfig result = (VaultConfig) method.invoke(builder, nullClusterConfig); - - Assert.assertEquals(clusterID, result.getClusterId()); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } catch (Exception e) { - Assert.fail("Reflection failed: " + e.getMessage()); - } - } - @Test public void testFindAndUpdateConnectionConfig_withNullConnectionUrl_fallsBackToPreviousUrl() { // `findAndUpdateConnectionConfig` has a ternary for connectionUrl that falls back diff --git a/src/test/java/com/skyflow/VaultClientTests.java b/skyvault/src/test/java/com/skyflow/VaultClientTests.java similarity index 90% rename from src/test/java/com/skyflow/VaultClientTests.java rename to skyvault/src/test/java/com/skyflow/VaultClientTests.java index d98f5964..4c040ad0 100644 --- a/src/test/java/com/skyflow/VaultClientTests.java +++ b/skyvault/src/test/java/com/skyflow/VaultClientTests.java @@ -1,12 +1,15 @@ package com.skyflow; +import com.skyflow.config.VaultConfig; +import com.skyflow.config.BaseCredentials; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.enums.*; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.SkyflowException; -import com.skyflow.generated.rest.resources.files.FilesClient; import com.skyflow.generated.rest.resources.files.requests.*; +import com.skyflow.generated.rest.resources.files.FilesClient; +import com.skyflow.generated.rest.types.*; import com.skyflow.generated.rest.resources.query.QueryClient; import com.skyflow.generated.rest.resources.records.RecordsClient; import com.skyflow.generated.rest.resources.records.requests.RecordServiceBatchOperationBody; @@ -18,7 +21,6 @@ import com.skyflow.generated.rest.resources.tokens.TokensClient; import com.skyflow.generated.rest.resources.tokens.requests.V1DetokenizePayload; import com.skyflow.generated.rest.resources.tokens.requests.V1TokenizePayload; -import com.skyflow.generated.rest.types.*; import com.skyflow.vault.data.InsertRequest; import com.skyflow.vault.data.UpdateRequest; import com.skyflow.vault.detect.*; @@ -707,7 +709,7 @@ public void testGetDeidentifyTextFileRequest() { String vaultId = "vault123"; String base64Content = "base64string"; - com.skyflow.generated.rest.resources.files.requests.DeidentifyFileRequestDeidentifyText textRequest = + DeidentifyFileRequestDeidentifyText textRequest = vaultClient.getDeidentifyTextFileRequest(request, vaultId, base64Content); Assert.assertEquals(vaultId, textRequest.getVaultId()); @@ -899,44 +901,16 @@ public void testMapAudioDataFormat_invalid() throws Exception { } } - @Test - public void testPrioritiseCredentials_VaultConfigCredentials() throws Exception { - Credentials creds = new Credentials(); - creds.setApiKey("test_api_key"); - vaultConfig.setCredentials(creds); - - java.lang.reflect.Method method = VaultClient.class.getDeclaredMethod("prioritiseCredentials"); - method.setAccessible(true); - method.invoke(vaultClient); - - Assert.assertEquals(creds, getPrivateField(vaultClient, "finalCredentials")); - } - - @Test - public void testPrioritiseCredentials_CommonCredentials() throws Exception { - vaultConfig.setCredentials(null); - Credentials creds = new Credentials(); - creds.setApiKey("common_api_key"); - setPrivateField(vaultClient, "commonCredentials", creds); - - java.lang.reflect.Method method = VaultClient.class.getDeclaredMethod("prioritiseCredentials"); - method.setAccessible(true); - method.invoke(vaultClient); - - Assert.assertEquals(creds, getPrivateField(vaultClient, "finalCredentials")); - } - - // Helper methods for reflection field access - private Object getPrivateField(Object obj, String fieldName) throws Exception { - java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(obj); - } - - private void setPrivateField(Object obj, String fieldName, Object value) throws Exception { - java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(obj, value); + // Fields moved to base classes (e.g. common.BaseVaultClient) aren't found by + // getDeclaredField on the subclass, so walk up the hierarchy. + private java.lang.reflect.Field findDeclaredField(Class clazz, String fieldName) throws NoSuchFieldException { + for (Class current = clazz; current != null; current = current.getSuperclass()) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException ignored) { + } + } + throw new NoSuchFieldException(fieldName); } @Test @@ -956,31 +930,6 @@ public void testGetFileForFileUpload_withFileObject() { } } - @Test - public void testSetBearerToken_validNonExpiredToken_reusesToken() { - try { - // far-future JWT: header.payload.sig where payload base64 decodes to {"exp":9999999999} - Credentials creds = new Credentials(); - creds.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultID); - config.setClusterId(clusterID); - config.setEnv(com.skyflow.enums.Env.DEV); - config.setCredentials(creds); - VaultClient freshClient = new VaultClient(config, null); - - // First call: token=null → generates from creds.getToken() - freshClient.setBearerToken(); - Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", getPrivateField(freshClient, "token")); - - // Second call: token valid, not expired → REUSE_BEARER_TOKEN else branch - freshClient.setBearerToken(); - Assert.assertEquals("x.eyJleHAiOjk5OTk5OTk5OTl9.y", getPrivateField(freshClient, "token")); - } catch (Exception e) { - Assert.fail("Should not have thrown: " + e.getMessage()); - } - } - @Test public void testGetDeidentifyImageRequest_withMaskingMethod() { try { @@ -1042,52 +991,6 @@ public void testGetDeIdentifyTextResponse_withEntityScores() { Assert.assertEquals(0.95, result.getEntities().get(0).getScores().get("EMAIL_ADDRESS"), 0.001); } - @Test - public void testPrioritiseCredentials_credentialChange_resetsTokenAndApiKey() { - try { - Credentials credentialsA = new Credentials(); - credentialsA.setToken("x.eyJleHAiOjk5OTk5OTk5OTl9.y"); - VaultConfig config = new VaultConfig(); - config.setVaultId("isolated-vault-change"); - config.setClusterId(clusterID); - config.setEnv(com.skyflow.enums.Env.DEV); - config.setCredentials(credentialsA); - VaultClient freshClient = new VaultClient(config, null); - - freshClient.updateVaultConfig(); // sets finalCredentials = credentialsA - setPrivateField(freshClient, "token", "cached-token"); // simulate prior auth - - Credentials credentialsB = new Credentials(); - credentialsB.setToken("other-token"); - config.setCredentials(credentialsB); - - freshClient.updateVaultConfig(); // original=A, new=B → different → reset token/apiKey - Assert.assertNull(getPrivateField(freshClient, "token")); - Assert.assertNull(getPrivateField(freshClient, "apiKey")); - } catch (Exception e) { - Assert.fail("Should not have thrown: " + e.getMessage()); - } - } - - @Test - public void testSetBearerToken_noCredentials_throwsEmptyCredentials() { - VaultConfig config = new VaultConfig(); - config.setVaultId("isolated-vault-nocreds"); - config.setClusterId(clusterID); - config.setEnv(com.skyflow.enums.Env.DEV); - // No credentials — will hit dotenv path → DotenvException → SkyflowException(EmptyCredentials) - VaultClient freshClient = new VaultClient(config, null); - try { - freshClient.setBearerToken(); - Assert.fail("Should have thrown SkyflowException"); - } catch (SkyflowException e) { - // SkyflowException expected — message varies by environment - // (EmptyCredentials when no .env, or credential error when .env provides creds) - } catch (Exception e) { - Assert.fail("Expected SkyflowException, got: " + e.getClass().getName() + ": " + e.getMessage()); - } - } - @Test public void testUpdateExecutorInHTTP_interceptorAddsAuthorizationHeader() { try { @@ -1103,7 +1006,7 @@ public void testUpdateExecutorInHTTP_interceptorAddsAuthorizationHeader() { freshClient.setBearerToken(); // triggers updateExecutorInHTTP → creates sharedHttpClient with interceptor // Access sharedHttpClient via reflection - java.lang.reflect.Field field = VaultClient.class.getDeclaredField("sharedHttpClient"); + java.lang.reflect.Field field = findDeclaredField(VaultClient.class, "sharedHttpClient"); field.setAccessible(true); OkHttpClient httpClient = (OkHttpClient) field.get(freshClient); Assert.assertNotNull(httpClient); diff --git a/src/test/java/com/skyflow/config/ConnectionConfigTests.java b/skyvault/src/test/java/com/skyflow/config/ConnectionConfigTests.java similarity index 100% rename from src/test/java/com/skyflow/config/ConnectionConfigTests.java rename to skyvault/src/test/java/com/skyflow/config/ConnectionConfigTests.java diff --git a/src/test/java/com/skyflow/config/ManagementConfigTest.java b/skyvault/src/test/java/com/skyflow/config/ManagementConfigTest.java similarity index 100% rename from src/test/java/com/skyflow/config/ManagementConfigTest.java rename to skyvault/src/test/java/com/skyflow/config/ManagementConfigTest.java diff --git a/src/test/java/com/skyflow/config/VaultConfigTests.java b/skyvault/src/test/java/com/skyflow/config/VaultConfigTests.java similarity index 100% rename from src/test/java/com/skyflow/config/VaultConfigTests.java rename to skyvault/src/test/java/com/skyflow/config/VaultConfigTests.java diff --git a/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java b/skyvault/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java similarity index 100% rename from src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java rename to skyvault/src/test/java/com/skyflow/enums/DeidentifyFileStatusTest.java diff --git a/src/test/java/com/skyflow/enums/DetectEntitiesTest.java b/skyvault/src/test/java/com/skyflow/enums/DetectEntitiesTest.java similarity index 100% rename from src/test/java/com/skyflow/enums/DetectEntitiesTest.java rename to skyvault/src/test/java/com/skyflow/enums/DetectEntitiesTest.java diff --git a/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java b/skyvault/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java similarity index 100% rename from src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java rename to skyvault/src/test/java/com/skyflow/enums/DetectOutputTranscriptionsTest.java diff --git a/src/test/java/com/skyflow/enums/MaskingMethodTest.java b/skyvault/src/test/java/com/skyflow/enums/MaskingMethodTest.java similarity index 100% rename from src/test/java/com/skyflow/enums/MaskingMethodTest.java rename to skyvault/src/test/java/com/skyflow/enums/MaskingMethodTest.java diff --git a/src/test/java/com/skyflow/enums/TokenModeTest.java b/skyvault/src/test/java/com/skyflow/enums/TokenModeTest.java similarity index 98% rename from src/test/java/com/skyflow/enums/TokenModeTest.java rename to skyvault/src/test/java/com/skyflow/enums/TokenModeTest.java index 72236a16..b65a72cb 100644 --- a/src/test/java/com/skyflow/enums/TokenModeTest.java +++ b/skyvault/src/test/java/com/skyflow/enums/TokenModeTest.java @@ -1,6 +1,5 @@ package com.skyflow.enums; -import com.skyflow.enums.LogLevel; import com.skyflow.generated.rest.types.V1Byot; import com.skyflow.logs.InfoLogs; import com.skyflow.utils.logger.LogUtil; diff --git a/src/test/java/com/skyflow/enums/TokenTypeTest.java b/skyvault/src/test/java/com/skyflow/enums/TokenTypeTest.java similarity index 100% rename from src/test/java/com/skyflow/enums/TokenTypeTest.java rename to skyvault/src/test/java/com/skyflow/enums/TokenTypeTest.java diff --git a/src/test/java/com/skyflow/utils/HttpUtilityTests.java b/skyvault/src/test/java/com/skyflow/utils/HttpUtilityTests.java similarity index 100% rename from src/test/java/com/skyflow/utils/HttpUtilityTests.java rename to skyvault/src/test/java/com/skyflow/utils/HttpUtilityTests.java diff --git a/skyvault/src/test/java/com/skyflow/utils/UtilsTests.java b/skyvault/src/test/java/com/skyflow/utils/UtilsTests.java new file mode 100644 index 00000000..d1e1c725 --- /dev/null +++ b/skyvault/src/test/java/com/skyflow/utils/UtilsTests.java @@ -0,0 +1,94 @@ +package com.skyflow.utils; + +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.enums.RequestMethod; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class UtilsTests { + private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; + private static String connectionId = null; + private static String connectionUrl = null; + private static Map queryParams; + private static Map pathParams; + private static Map requestHeaders; + + @BeforeClass + public static void setup() { + connectionId = "test_connection_id"; + connectionUrl = "https://test.connection.url"; + pathParams = new HashMap<>(); + queryParams = new HashMap<>(); + requestHeaders = new HashMap<>(); + } + + @Test + public void testConstructConnectionURL() { + try { + queryParams.put("query_param", "value"); + pathParams.put("path_param", "value"); + + ConnectionConfig connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId(connectionId); + connectionConfig.setConnectionUrl(connectionUrl); + + InvokeConnectionRequest request = InvokeConnectionRequest.builder() + .method(RequestMethod.POST).pathParams(pathParams).queryParams(queryParams).build(); + String filledUrl = Utils.constructConnectionURL(connectionConfig, request); + Assert.assertEquals(connectionUrl + "?" + "query_param=value", filledUrl); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testConstructConnectionHeaderMap() { + try { + requestHeaders.put("HEADER", "value"); + Map headers = Utils.constructConnectionHeadersMap(requestHeaders); + Assert.assertEquals(1, headers.size()); + Assert.assertTrue(headers.containsKey("header")); + Assert.assertFalse(headers.containsKey("HEADER")); + Assert.assertEquals("value", headers.get("header")); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetMetrics() { + try { + JsonObject metrics = Utils.getMetrics(); + Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_NAME_VERSION)); + Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_CLIENT_DEVICE_MODEL)); + Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_CLIENT_OS_DETAILS)); + Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_RUNTIME_DETAILS)); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + public void testGetMetricsWithException() { + try { + // Clearing System Properties explicitly to throw exception + System.clearProperty("os.name"); + System.clearProperty("os.version"); + System.clearProperty("java.version"); + + JsonObject metrics = Utils.getMetrics(); + Assert.assertEquals("skyflow-java@" + Constants.SDK_VERSION, metrics.get(Constants.SDK_METRIC_NAME_VERSION).getAsString()); + Assert.assertEquals("Java@", metrics.get(Constants.SDK_METRIC_RUNTIME_DETAILS).getAsString()); + Assert.assertTrue(metrics.get(Constants.SDK_METRIC_CLIENT_DEVICE_MODEL).getAsString().isEmpty()); + Assert.assertTrue(metrics.get(Constants.SDK_METRIC_CLIENT_OS_DETAILS).getAsString().isEmpty()); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } +} diff --git a/src/test/java/com/skyflow/vault/BinAuditTests.java b/skyvault/src/test/java/com/skyflow/vault/BinAuditTests.java similarity index 100% rename from src/test/java/com/skyflow/vault/BinAuditTests.java rename to skyvault/src/test/java/com/skyflow/vault/BinAuditTests.java diff --git a/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 src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java rename to skyvault/src/test/java/com/skyflow/vault/connection/InvokeConnectionTests.java diff --git a/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 src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/ConnectionControllerTests.java diff --git a/skyvault/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java new file mode 100644 index 00000000..b44c3310 --- /dev/null +++ b/skyvault/src/test/java/com/skyflow/vault/controller/ConnectionRequestFidelityTests.java @@ -0,0 +1,267 @@ +package com.skyflow.vault.controller; + +import com.google.gson.JsonObject; +import com.skyflow.config.ConnectionConfig; +import com.skyflow.config.Credentials; +import com.skyflow.enums.RequestMethod; +import com.skyflow.utils.Constants; +import com.skyflow.utils.HttpUtility; +import com.skyflow.vault.connection.InvokeConnectionRequest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Request-fidelity tests for {@code ConnectionController.invoke} at the transport boundary: every + * value the user sets on {@link InvokeConnectionRequest} is captured as it is handed to + * {@link HttpUtility#sendRequest}. + * + *

Tests suffixed {@code _knownGap} pin confirmed defects and assert the CURRENT behaviour. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({HttpUtility.class}) +public class ConnectionRequestFidelityTests { + + private static final String API_KEY = "sky-ab123-abcd1234cdef1234abcd4321cdef4321"; // gitleaks:allow + private static final String CONNECTION_URL = "https://conn.example.com/api/{resource}/details"; + private static final String NON_ASCII_NAME = "日本語 テスト"; + + private static ConnectionConfig connectionConfig; + private static Credentials credentials; + private ConnectionController controller; + + @BeforeClass + public static void setupClass() { + credentials = new Credentials(); + credentials.setApiKey(API_KEY); + + connectionConfig = new ConnectionConfig(); + connectionConfig.setConnectionId("conn123"); + connectionConfig.setConnectionUrl(CONNECTION_URL); + connectionConfig.setCredentials(credentials); + } + + @Before + public void setup() throws Exception { + controller = new ConnectionController(connectionConfig, credentials); + PowerMockito.mockStatic(HttpUtility.class); + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())).thenReturn("{}"); + when(HttpUtility.getRequestID()).thenReturn("req-fidelity-1"); + } + + private static final class Captured { + private String method; + private URL url; + private JsonObject body; + private Map headers; + } + + @SuppressWarnings("unchecked") + private static Captured capture() throws Exception { + ArgumentCaptor methodCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(JsonObject.class); + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(Map.class); + + PowerMockito.verifyStatic(HttpUtility.class); + HttpUtility.sendRequest(methodCaptor.capture(), urlCaptor.capture(), + bodyCaptor.capture(), headersCaptor.capture()); + + Captured captured = new Captured(); + captured.method = methodCaptor.getValue(); + captured.url = urlCaptor.getValue(); + captured.body = bodyCaptor.getValue(); + captured.headers = headersCaptor.getValue(); + return captured; + } + + @Test + public void testInvoke_methodPathParamsQueryParamsHeadersAndBodyAllReachTransport() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + + Map queryParams = new LinkedHashMap<>(); + queryParams.put("limit", "10"); + queryParams.put("offset", "20"); + + Map requestHeaders = new LinkedHashMap<>(); + requestHeaders.put("X-Custom-Header", NON_ASCII_NAME); + requestHeaders.put("content-type", "application/json"); + + Map nested = new LinkedHashMap<>(); + nested.put("city", "北京市 朝阳区"); + nested.put("zip", "100000"); + + Map requestBody = new LinkedHashMap<>(); + requestBody.put("name", NON_ASCII_NAME); + requestBody.put("age", 42); + requestBody.put("active", true); + requestBody.put("address", nested); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.PUT) + .pathParams(pathParams) + .queryParams(queryParams) + .requestHeaders(requestHeaders) + .requestBody(requestBody) + .build()); + + Captured captured = capture(); + + Assert.assertEquals("PUT", captured.method); + Assert.assertEquals("https://conn.example.com/api/cards/details?limit=10&offset=20", + captured.url.toString()); + + Assert.assertEquals(NON_ASCII_NAME, captured.headers.get("x-custom-header")); + Assert.assertEquals("application/json", captured.headers.get("content-type")); + Assert.assertEquals("auth header carries the api key", + API_KEY, captured.headers.get(Constants.SDK_AUTH_HEADER_KEY)); + + Assert.assertEquals(NON_ASCII_NAME, captured.body.get("name").getAsString()); + Assert.assertEquals(42, captured.body.get("age").getAsInt()); + Assert.assertTrue(captured.body.get("active").getAsBoolean()); + Assert.assertEquals("北京市 朝阳区", + captured.body.getAsJsonObject("address").get("city").getAsString()); + Assert.assertEquals("100000", + captured.body.getAsJsonObject("address").get("zip").getAsString()); + } + + @Test + public void testInvoke_defaultMethodIsPost() throws Exception { + controller.invoke(InvokeConnectionRequest.builder().build()); + Assert.assertEquals("POST", capture().method); + } + + @Test + public void testInvoke_eachRequestMethodIsForwardedByName() throws Exception { + for (RequestMethod method : new RequestMethod[]{ + RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE}) { + ConnectionController freshController = new ConnectionController(connectionConfig, credentials); + PowerMockito.mockStatic(HttpUtility.class); + when(HttpUtility.sendRequest(anyString(), any(URL.class), any(), any())).thenReturn("{}"); + when(HttpUtility.getRequestID()).thenReturn("req-fidelity-1"); + + freshController.invoke(InvokeConnectionRequest.builder().method(method).build()); + Assert.assertEquals(method.name(), capture().method); + } + } + + /** + * NEW FINDING (not one of the 11 audited gaps): a non-object {@code requestBody} — a String, + * a number, a List — never reaches the transport. {@code Validations} calls + * {@code gson.toJsonTree(body).getAsJsonObject()} and blows up with a raw + * {@link IllegalStateException} (not a {@code SkyflowException}), which also makes + * {@code ConnectionController.convertObjectToJson}'s "wrap scalars under a value key" branch + * unreachable for scalars. + */ + @Test + public void testInvoke_scalarRequestBodyThrowsRawIllegalStateException_knownGap() throws Exception { + for (Object body : new Object[]{"a plain string", 42, java.util.Arrays.asList("a", "b")}) { + try { + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.POST) + .requestBody(body) + .build()); + Assert.fail("expected an exception for a non-object request body: " + body); + } catch (IllegalStateException expected) { + Assert.assertTrue(expected.getMessage().contains("Not a JSON Object")); + } + } + PowerMockito.verifyStatic(HttpUtility.class, org.mockito.Mockito.never()); + HttpUtility.sendRequest(anyString(), any(URL.class), any(), any()); + } + + /** + * KNOWN GAP: a user-supplied {@code sky-metadata} header is unconditionally overwritten by the + * SDK's own metrics blob. + */ + @Test + public void testInvoke_userSuppliedSkyMetadataHeaderIsOverwritten_knownGap() throws Exception { + Map requestHeaders = new HashMap<>(); + requestHeaders.put(Constants.SDK_METRICS_HEADER_KEY, "user-supplied-value"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .requestHeaders(requestHeaders) + .build()); + + String sent = capture().headers.get(Constants.SDK_METRICS_HEADER_KEY); + Assert.assertNotEquals("user value is discarded", "user-supplied-value", sent); + Assert.assertTrue("replaced by the SDK metrics blob", + sent.contains(Constants.SDK_METRIC_NAME_VERSION)); + } + + /** + * KNOWN GAP: two request headers differing only in case collapse into one entry before they + * reach the transport (last writer wins), so one of the user's headers is lost. + */ + @Test + public void testInvoke_headersDifferingOnlyInCaseCollapse_knownGap() throws Exception { + Map requestHeaders = new LinkedHashMap<>(); + requestHeaders.put("X-Trace", "first-value"); + requestHeaders.put("x-trace", "second-value"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .requestHeaders(requestHeaders) + .build()); + + Map headers = capture().headers; + Assert.assertEquals("second-value", headers.get("x-trace")); + Assert.assertFalse("the original-cased key is gone", headers.containsKey("X-Trace")); + } + + /** + * KNOWN GAP: query-param values are not percent-encoded, so a value containing {@code &} and + * {@code =} re-partitions the query string on the wire. + */ + @Test + public void testInvoke_queryParamValuesReachTransportUnencoded_knownGap() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards"); + Map queryParams = new LinkedHashMap<>(); + queryParams.put("q", "a&b=c"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .pathParams(pathParams) + .queryParams(queryParams) + .build()); + + URL url = capture().url; + Assert.assertEquals("q=a&b=c", url.getQuery()); + Assert.assertEquals("one user param became two wire params", 2, url.getQuery().split("&").length); + } + + /** + * KNOWN GAP: path-param values are not percent-encoded, so a {@code /} in the value changes the + * request path on the wire. + */ + @Test + public void testInvoke_pathParamValuesReachTransportUnencoded_knownGap() throws Exception { + Map pathParams = new LinkedHashMap<>(); + pathParams.put("resource", "cards/123"); + + controller.invoke(InvokeConnectionRequest.builder() + .method(RequestMethod.GET) + .pathParams(pathParams) + .build()); + + Assert.assertEquals("/api/cards/123/details", capture().url.getPath()); + } +} diff --git a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java b/skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java similarity index 99% rename from src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java index 2b8379bc..f0347eeb 100644 --- a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java @@ -1,5 +1,6 @@ package com.skyflow.vault.controller; +import com.skyflow.config.BaseVaultConfig; import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; import com.skyflow.errors.ErrorCode; diff --git a/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 src/test/java/com/skyflow/vault/controller/DetectControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/DetectControllerTests.java diff --git a/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 src/test/java/com/skyflow/vault/controller/VaultControllerTests.java rename to skyvault/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java diff --git a/src/test/java/com/skyflow/vault/data/DeleteTests.java b/skyvault/src/test/java/com/skyflow/vault/data/DeleteTests.java similarity index 98% rename from src/test/java/com/skyflow/vault/data/DeleteTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/DeleteTests.java index befc3d26..ae294f24 100644 --- a/src/test/java/com/skyflow/vault/data/DeleteTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/data/DeleteTests.java @@ -1,8 +1,8 @@ package com.skyflow.vault.data; import com.skyflow.Skyflow; +import com.skyflow.config.BaseVaultConfig; import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; import com.skyflow.enums.Env; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; @@ -35,7 +35,7 @@ public static void setup() { Credentials credentials = new Credentials(); credentials.setToken("valid-token"); - VaultConfig vaultConfig = new VaultConfig(); + BaseVaultConfig vaultConfig = new BaseVaultConfig(); vaultConfig.setVaultId(vaultID); vaultConfig.setClusterId(clusterID); vaultConfig.setEnv(Env.DEV); diff --git a/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 src/test/java/com/skyflow/vault/data/FileUploadTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/FileUploadTests.java diff --git a/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 src/test/java/com/skyflow/vault/data/GetTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/GetTests.java index e62c605f..bc6f24ef 100644 --- a/src/test/java/com/skyflow/vault/data/GetTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/data/GetTests.java @@ -1,8 +1,8 @@ package com.skyflow.vault.data; import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.config.Credentials; import com.skyflow.enums.Env; import com.skyflow.enums.RedactionType; import com.skyflow.errors.ErrorCode; diff --git a/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 src/test/java/com/skyflow/vault/data/InsertTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/InsertTests.java index 030bc5ea..0ba6c7f8 100644 --- a/src/test/java/com/skyflow/vault/data/InsertTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/data/InsertTests.java @@ -1,8 +1,8 @@ package com.skyflow.vault.data; import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.config.Credentials; import com.skyflow.enums.Env; import com.skyflow.enums.TokenMode; import com.skyflow.errors.ErrorCode; diff --git a/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 src/test/java/com/skyflow/vault/data/QueryResponseTest.java rename to skyvault/src/test/java/com/skyflow/vault/data/QueryResponseTest.java diff --git a/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 src/test/java/com/skyflow/vault/data/QueryTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/QueryTests.java diff --git a/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 src/test/java/com/skyflow/vault/data/UpdateTests.java rename to skyvault/src/test/java/com/skyflow/vault/data/UpdateTests.java index 8ae1db8d..ca334976 100644 --- a/src/test/java/com/skyflow/vault/data/UpdateTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/data/UpdateTests.java @@ -1,8 +1,8 @@ package com.skyflow.vault.data; import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.config.Credentials; import com.skyflow.enums.Env; import com.skyflow.enums.TokenMode; import com.skyflow.errors.ErrorCode; diff --git a/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 src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileRequestTest.java diff --git a/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 src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyFileResponseTest.java diff --git a/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 src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java rename to skyvault/src/test/java/com/skyflow/vault/detect/DeidentifyTextTests.java diff --git a/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 src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/FileEntityInfoTest.java diff --git a/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 src/test/java/com/skyflow/vault/detect/FileInfoTest.java rename to skyvault/src/test/java/com/skyflow/vault/detect/FileInfoTest.java diff --git a/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 src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java rename to skyvault/src/test/java/com/skyflow/vault/detect/ReidentifyTextTests.java diff --git a/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 src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java rename to skyvault/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java diff --git a/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 src/test/java/com/skyflow/vault/tokens/TokenizeTests.java rename to skyvault/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java index 4c279fde..27563a57 100644 --- a/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java +++ b/skyvault/src/test/java/com/skyflow/vault/tokens/TokenizeTests.java @@ -1,8 +1,8 @@ package com.skyflow.vault.tokens; import com.skyflow.Skyflow; -import com.skyflow.config.Credentials; import com.skyflow.config.VaultConfig; +import com.skyflow.config.Credentials; import com.skyflow.enums.Env; import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; diff --git a/skyvault/src/test/resources/notJson.txt b/skyvault/src/test/resources/notJson.txt new file mode 100644 index 00000000..bdf08de0 --- /dev/null +++ b/skyvault/src/test/resources/notJson.txt @@ -0,0 +1 @@ +test file \ No newline at end of file diff --git a/src/main/java/com/skyflow/utils/Constants.java b/src/main/java/com/skyflow/utils/Constants.java deleted file mode 100644 index aa3a3f14..00000000 --- a/src/main/java/com/skyflow/utils/Constants.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.skyflow.utils; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -public final class Constants { - public static final String SECURE_PROTOCOL = "https://"; - public static final String DEV_DOMAIN = ".vault.skyflowapis.dev"; - public static final String STAGE_DOMAIN = ".vault.skyflowapis.tech"; - public static final String SANDBOX_DOMAIN = ".vault.skyflowapis-preview.com"; - public static final String PROD_DOMAIN = ".vault.skyflowapis.com"; - public static final String PKCS8_PRIVATE_HEADER = "-----BEGIN PRIVATE KEY-----"; - public static final String PKCS8_PRIVATE_FOOTER = "-----END PRIVATE KEY-----"; - public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - public static final String SIGNED_DATA_TOKEN_PREFIX = "signed_token_"; - public static final String ORDER_ASCENDING = "ASCENDING"; - public static final String API_KEY_REGEX = "^sky-[a-zA-Z0-9]{5}-[a-fA-F0-9]{32}$"; - public static final String CONTEXT_KEY_REGEX = "^[a-zA-Z0-9_]+$"; - public static final String ENV_CREDENTIALS_KEY_NAME = "SKYFLOW_CREDENTIALS"; - public static final String SDK_NAME = "Skyflow Java SDK"; - public static final String DEFAULT_SDK_VERSION = "v2"; - public static final String SDK_VERSION; - public static final String SDK_PREFIX; - public static final String SDK_METRIC_NAME_VERSION = "sdk_name_version"; - public static final String SDK_METRIC_NAME_VERSION_PREFIX = "skyflow-java@"; - public static final String SDK_METRIC_CLIENT_DEVICE_MODEL = "sdk_client_device_model"; - public static final String SDK_METRIC_CLIENT_OS_DETAILS = "sdk_client_os_details"; - public static final String SDK_METRIC_RUNTIME_DETAILS = "sdk_runtime_details"; - public static final String SDK_METRIC_RUNTIME_DETAILS_PREFIX = "Java@"; - public static final String SDK_AUTH_HEADER_KEY = "x-skyflow-authorization"; - public static final String SDK_METRICS_HEADER_KEY = "sky-metadata"; - public static final String REQUEST_ID_HEADER_KEY = "x-request-id"; - public static final String PROCESSED_FILE_NAME_PREFIX = "processed-"; - public static final String ERROR_FROM_CLIENT_HEADER_KEY = "error-from-client"; - public static final String DEIDENTIFIED_FILE_PREFIX = "deidentified"; - public static final String HTTPS_PROTOCOL = "https"; - public static final String CURLY_PLACEHOLDER = "{%s}"; - public static final String EMPTY_STRING = ""; - public static final String QUOTE = "\""; - - public static final class HttpUtilityExtra { - public static final String SDK_GENERATED_PREFIX = "SDK-Generated-"; - private HttpUtilityExtra() {} - } - - static { - String sdkVersion; - // Use a static initializer block to read the properties file - Properties properties = new Properties(); - try (InputStream input = Constants.class.getClassLoader().getResourceAsStream("sdk.properties")) { - if (input == null) { - sdkVersion = DEFAULT_SDK_VERSION; - } else { - properties.load(input); - sdkVersion = properties.getProperty("sdk.version", DEFAULT_SDK_VERSION); - } - } catch (IOException ex) { - sdkVersion = DEFAULT_SDK_VERSION; - } - SDK_VERSION = sdkVersion; - SDK_PREFIX = SDK_NAME + " " + SDK_VERSION; - } -} diff --git a/src/test/java/com/skyflow/VaultClientDotenvTests.java b/src/test/java/com/skyflow/VaultClientDotenvTests.java deleted file mode 100644 index 1a54b13d..00000000 --- a/src/test/java/com/skyflow/VaultClientDotenvTests.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.skyflow; - -import com.skyflow.config.Credentials; -import com.skyflow.config.VaultConfig; -import com.skyflow.enums.Env; -import com.skyflow.errors.ErrorMessage; -import com.skyflow.errors.SkyflowException; -import com.skyflow.utils.Constants; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - -/** - * Tests for VaultClient's prioritiseCredentials dotenv path. - * - * These tests write a temporary .env file to exercise the code path where - * no VaultConfig credentials and no common credentials are set, so the code - * falls through to read from a .env file. - */ -public class VaultClientDotenvTests { - - private static final String ENV_FILE = ".env"; - private byte[] originalEnvContent; - - @Before - public void saveEnvFileState() throws IOException { - File f = new File(ENV_FILE); - originalEnvContent = f.exists() ? Files.readAllBytes(Paths.get(ENV_FILE)) : null; - } - - @After - public void restoreEnvFile() throws IOException { - if (originalEnvContent != null) { - Files.write(Paths.get(ENV_FILE), originalEnvContent); - } else { - Files.deleteIfExists(Paths.get(ENV_FILE)); - } - } - - private VaultClient buildClientWithNoCreds(String vaultId, String clusterId) { - VaultConfig config = new VaultConfig(); - config.setVaultId(vaultId); - config.setClusterId(clusterId); - config.setEnv(Env.DEV); - // No credentials set - return new VaultClient(config, null); - } - - /** - * Covers the dotenv success path: Dotenv.load() succeeds and returns a - * non-null credentials string, so finalCredentials is set via - * credentialsString. Lines ~862-870 of VaultClient.java. - */ - @Test - public void testPrioritiseCredentials_dotenvReturnsCredentials_setsCredentials() throws Exception { - // Write a .env file with a valid credentials string value - try (FileWriter fw = new FileWriter(ENV_FILE)) { - fw.write(Constants.ENV_CREDENTIALS_KEY_NAME + "={\"token\":\"env-token-value\"}\n"); - } - - VaultClient client = buildClientWithNoCreds("dotenv-vault-1", "cluster1"); - // updateVaultConfig() calls prioritiseCredentials() which reads from .env - // Should not throw since sysCredentials is non-null - client.updateVaultConfig(); - - // finalCredentials should be set with credentials string - java.lang.reflect.Field field = VaultClient.class.getDeclaredField("finalCredentials"); - field.setAccessible(true); - Credentials finalCreds = (Credentials) field.get(client); - Assert.assertNotNull(finalCreds); - Assert.assertEquals("{\"token\":\"env-token-value\"}", finalCreds.getCredentialsString()); - } - - /** - * Covers the path where dotenv loads but the key is absent (returns null), - * causing SkyflowException(EmptyCredentials) to be thrown directly. - * Lines ~864-876 of VaultClient.java. - */ - @Test - public void testPrioritiseCredentials_dotenvKeyMissing_throwsSkyflowException() throws Exception { - // Write a .env file WITHOUT the SKYFLOW_CREDENTIALS key - try (FileWriter fw = new FileWriter(ENV_FILE)) { - fw.write("SOME_OTHER_KEY=some_value\n"); - } - - VaultClient client = buildClientWithNoCreds("dotenv-vault-2", "cluster2"); - try { - client.updateVaultConfig(); - Assert.fail("Should have thrown SkyflowException"); - } catch (SkyflowException e) { - Assert.assertTrue(e.getMessage().contains(ErrorMessage.EmptyCredentials.getMessage())); - } catch (RuntimeException e) { - Assert.fail("Expected direct SkyflowException, not RuntimeException wrapping it"); - } - } -} diff --git a/src/test/java/com/skyflow/utils/UtilsTests.java b/src/test/java/com/skyflow/utils/UtilsTests.java deleted file mode 100644 index 4e910e15..00000000 --- a/src/test/java/com/skyflow/utils/UtilsTests.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.skyflow.utils; - -import com.google.gson.JsonObject; -import com.skyflow.config.ConnectionConfig; -import com.skyflow.config.Credentials; -import com.skyflow.enums.Env; -import com.skyflow.enums.RequestMethod; -import com.skyflow.errors.ErrorCode; -import com.skyflow.errors.ErrorMessage; -import com.skyflow.errors.SkyflowException; -import com.skyflow.vault.connection.InvokeConnectionRequest; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -public class UtilsTests { - private static final String INVALID_EXCEPTION_THROWN = "Should not have thrown any exception"; - private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; - private static String clusterId = null; - private static String url = null; - private static String filePath = null; - private static String credentialsString = null; - private static String token = null; - private static String context = null; - private static ArrayList roles = null; - private static String connectionId = null; - private static String connectionUrl = null; - private static Map queryParams; - private static Map pathParams; - private static Map requestHeaders; - - @BeforeClass - public static void setup() { - clusterId = "test_cluster_id"; - url = "https://test-url.com/java/unit/tests"; - filePath = "invalid/file/path/credentials.json"; - credentialsString = "invalid credentials string"; - token = "invalid-token"; - context = "test_context"; - roles = new ArrayList<>(); - String role = "test_role"; - roles.add(role); - connectionId = "test_connection_id"; - connectionUrl = "https://test.connection.url"; - pathParams = new HashMap<>(); - queryParams = new HashMap<>(); - requestHeaders = new HashMap<>(); - } - - @Test - public void testGetVaultURLForDev() { - try { - String vaultURL = Utils.getVaultURL(clusterId, Env.DEV); - String devUrl = "https://test_cluster_id.vault.skyflowapis.dev"; - Assert.assertEquals(devUrl, vaultURL); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetVaultURLForStage() { - try { - String vaultURL = Utils.getVaultURL(clusterId, Env.STAGE); - String stageUrl = "https://test_cluster_id.vault.skyflowapis.tech"; - Assert.assertEquals(stageUrl, vaultURL); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetVaultURLForSandbox() { - try { - String vaultURL = Utils.getVaultURL(clusterId, Env.SANDBOX); - String sandboxUrl = "https://test_cluster_id.vault.skyflowapis-preview.com"; - Assert.assertEquals(sandboxUrl, vaultURL); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetVaultURLForProd() { - try { - String vaultURL = Utils.getVaultURL(clusterId, Env.PROD); - String prodUrl = "https://test_cluster_id.vault.skyflowapis.com"; - Assert.assertEquals(prodUrl, vaultURL); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetBaseURL() { - try { - String baseURL = Utils.getBaseURL(url); - String url = "https://test-url.com"; - Assert.assertEquals(url, baseURL); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGenerateBearerTokenWithCredentialsFile() { - try { - Credentials credentials = new Credentials(); - credentials.setPath(filePath); - credentials.setContext(context); - credentials.setRoles(roles); - Utils.generateBearerToken(credentials); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.FileNotFound.getMessage(), filePath), - e.getMessage() - ); - } - } - - @Test - public void testGenerateBearerTokenWithCredentialsString() { - try { - Credentials credentials = new Credentials(); - credentials.setCredentialsString(credentialsString); - credentials.setContext(context); - credentials.setRoles(roles); - Utils.generateBearerToken(credentials); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.CredentialsStringInvalidJson.getMessage(), e.getMessage()); - } - } - - @Test - public void testGenerateBearerTokenWithToken() { - try { - Credentials credentials = new Credentials(); - credentials.setToken(token); - credentials.setContext(context); - credentials.setRoles(roles); - String bearerToken = Utils.generateBearerToken(credentials); - Assert.assertEquals(token, bearerToken); - } catch (SkyflowException e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testConstructConnectionURL() { - try { - queryParams.put("query_param", "value"); - pathParams.put("path_param", "value"); - - ConnectionConfig connectionConfig = new ConnectionConfig(); - connectionConfig.setConnectionId(connectionId); - connectionConfig.setConnectionUrl(connectionUrl); - - InvokeConnectionRequest request = InvokeConnectionRequest.builder() - .method(RequestMethod.POST).pathParams(pathParams).queryParams(queryParams).build(); - String filledUrl = Utils.constructConnectionURL(connectionConfig, request); - Assert.assertEquals(connectionUrl + "?" + "query_param=value", filledUrl); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testConstructConnectionHeaderMap() { - try { - requestHeaders.put("HEADER", "value"); - Map headers = Utils.constructConnectionHeadersMap(requestHeaders); - Assert.assertEquals(1, headers.size()); - Assert.assertTrue(headers.containsKey("header")); - Assert.assertFalse(headers.containsKey("HEADER")); - Assert.assertEquals("value", headers.get("header")); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetMetrics() { - try { - JsonObject metrics = Utils.getMetrics(); - Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_NAME_VERSION)); - Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_CLIENT_DEVICE_MODEL)); - Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_CLIENT_OS_DETAILS)); - Assert.assertNotNull(metrics.get(Constants.SDK_METRIC_RUNTIME_DETAILS)); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } - - @Test - public void testGetMetricsWithException() { - try { - // Clearing System Properties explicitly to throw exception - System.clearProperty("os.name"); - System.clearProperty("os.version"); - System.clearProperty("java.version"); - - JsonObject metrics = Utils.getMetrics(); - Assert.assertEquals("skyflow-java@" + Constants.SDK_VERSION, metrics.get(Constants.SDK_METRIC_NAME_VERSION).getAsString()); - Assert.assertEquals("Java@", metrics.get(Constants.SDK_METRIC_RUNTIME_DETAILS).getAsString()); - Assert.assertTrue(metrics.get(Constants.SDK_METRIC_CLIENT_DEVICE_MODEL).getAsString().isEmpty()); - Assert.assertTrue(metrics.get(Constants.SDK_METRIC_CLIENT_OS_DETAILS).getAsString().isEmpty()); - } catch (Exception e) { - Assert.fail(INVALID_EXCEPTION_THROWN); - } - } -}