From 37720785d388e6e0180c87966572c2a970ae3cc6 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 22:57:27 +0530 Subject: [PATCH 01/48] docs: add Java SDK nomenclature cleanup design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the design for public interface renames per the server-side SDK nomenclature changes spec: credential field fallbacks (clientId/keyId/tokenUri), skyflow_id→skyflowId in Get/Query responses, and QueryResponse errors/tokenizedData field additions. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...-05-13-java-nomenclature-cleanup-design.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md new file mode 100644 index 00000000..2d194871 --- /dev/null +++ b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md @@ -0,0 +1,116 @@ +# Java SDK Nomenclature Cleanup — Design Spec + +**Date:** 2026-05-13 +**Reference:** [Skyflow Server-Side SDK: Nomenclature changes](https://skyflow.atlassian.net/wiki/spaces/SDK1/pages/2933162001/Skyflow+Server-Side+SDK+Nomenclature+changes) +**Scope:** Public interface only (`src/main/java/com/skyflow/`) +**Target release:** v3.0.0 (HIGH), v3.1.0 (MEDIUM), v3.1.x (LOW audit) + +--- + +## Summary of changes + +| Priority | Change | Files | +|---|---|---| +| HIGH | `clientID` → `clientId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | +| HIGH | `keyID` → `keyId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | +| HIGH | `tokenURI` → `tokenUri` in credentials JSON parsing (with fallback) | `BearerToken.java` | +| MEDIUM | `skyflow_id` → `skyflowId` key in Get and Query response maps | `VaultController.java` | +| MEDIUM | `tokenizedData` as a real field on `QueryResponse` (always present) | `QueryResponse.java` | +| MEDIUM | `getErrors()` added to `QueryResponse` (field was missing entirely) | `QueryResponse.java` | +| LOW | Audit all builder setter/getter names — confirm no `setFooID()` pattern | `VaultConfig.java`, request builders | + +--- + +## Detailed design + +### HIGH: Credentials JSON field renames + +**Affected:** `BearerToken.java` (method `getBearerTokenFromCredentials`), +`SignedDataTokens.java` (method `generateSignedTokensFromCredentials`) + +The credentials JSON file (provided by users) currently uses `clientID`, `keyID`, `tokenURI`. +The new canonical form is `clientId`, `keyId`, `tokenUri` (acronyms treated as words, per Java camelCase convention). + +**Strategy:** Try the new key first; fall back to the old key if null. This allows existing credentials files to keep working during migration. + +```java +// clientID → clientId +JsonElement clientId = credentials.get("clientId"); +if (clientId == null) clientId = credentials.get("clientID"); +if (clientId == null) { + throw new SkyflowException(...MissingClientId...); +} + +// keyID → keyId +JsonElement keyId = credentials.get("keyId"); +if (keyId == null) keyId = credentials.get("keyID"); +if (keyId == null) { + throw new SkyflowException(...MissingKeyId...); +} + +// tokenURI → tokenUri (BearerToken only) +JsonElement tokenUri = credentials.get("tokenUri"); +if (tokenUri == null) tokenUri = credentials.get("tokenURI"); +if (tokenUri == null) { + throw new SkyflowException(...MissingTokenUri...); +} +``` + +Local variable names and private method parameter names updated to match (`clientId`, `keyId`, `tokenUri`). + +--- + +### MEDIUM: skyflow_id → skyflowId in Get and Query response maps + +**Affected:** `VaultController.java` — `getFormattedGetRecord()` and `getFormattedQueryRecord()` + +Insert and Update responses already use `skyflowId`. Get and Query currently call `putAll(fieldsOpt.get())` which passes through the raw API field name `skyflow_id`. After the `putAll`, rename the key: + +```java +if (record.containsKey("skyflow_id")) { + record.put("skyflowId", record.remove("skyflow_id")); +} +``` + +Applied in both `getFormattedGetRecord` and `getFormattedQueryRecord`. + +--- + +### MEDIUM: tokenizedData always present in QueryResponse + +**Affected:** `QueryResponse.java` + +Currently `tokenizedData` is only injected inside `toString()` as a hack — it is not a real field on the object. A caller accessing the query result programmatically cannot retrieve tokenized data. + +**Fix:** Add `tokenizedData` as a proper field, populated during construction from the API response data. Default to an empty map when absent. Expose via `getTokenizedData()`. Remove the manual injection from `toString()`. + +The `toString()` override is simplified to use `serializeNulls` Gson directly on the object. + +--- + +### MEDIUM: errors always present in QueryResponse + +**Affected:** `QueryResponse.java` + +`QueryResponse` has no `errors` field or `getErrors()` method today — errors are only referenced in `toString()` as a hardcoded `null`. A caller cannot access errors programmatically. + +**Fix:** Add `private final ArrayList> errors` (always `null` — not converted to empty list) and a `getErrors()` accessor. Consistent with `GetResponse`, `InsertResponse`, `UpdateResponse` which all have `getErrors()` returning null when no errors. + +--- + +### LOW: Audit builder setter/getter names + +**Affected:** `VaultConfig.java`, `InsertRequest`, `UpdateRequest`, `GetRequest`, `DeleteRequest`, `FileUploadRequest`, `QueryRequest` + +Confirm all methods follow `setFooId()` / `getFooId()` (title-case `Id`), not `setFooID()` (all-caps `ID`). + +From initial review: `setVaultId()`, `setClusterId()` in `VaultConfig` are already correct. Full grep audit required to confirm no remaining violations. + +--- + +## What is NOT in scope + +- `UpdateRequest.getData()` map key convention (user passes `skyflow_id` to identify the record to update — this is an input key, not a response key, and is not addressed in the spec) +- Any changes to generated REST client code under `com.skyflow.generated.*` +- `SKYFLOW_CREDENTIALS` environment variable name (stays `ALL_CAPS` per OS convention) +- Validation logic changes (null insert value handling is Python-only per spec) From 9afb24a95b8d7f4bfcdf8bb5eb998f1223546a9a Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 23:22:04 +0530 Subject: [PATCH 02/48] docs: clarify tokenizedData reasoning in nomenclature cleanup spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds explanation for why tokenizedData change is valid despite the Query API currently not returning tokens — based on V1FieldRecords schema support and cross-SDK consistency requirement. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...-05-13-java-nomenclature-cleanup-design.md | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md index 2d194871..06840515 100644 --- a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md +++ b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md @@ -78,13 +78,28 @@ Applied in both `getFormattedGetRecord` and `getFormattedQueryRecord`. ### MEDIUM: tokenizedData always present in QueryResponse -**Affected:** `QueryResponse.java` +**Affected:** `VaultController.java` (`getFormattedQueryRecord`), `QueryResponse.java` + +**Why this change is valid:** + +The Skyflow API docs state that the Query endpoint "can't return tokens" today. However: + +1. The Fern-generated `V1FieldRecords` type explicitly defines a `tokens` field alongside `fields` — meaning the API contract already supports it and it may be populated in future. +2. The spec's cross-SDK requirement is that `tokenizedData` is always present per-record (even as an empty object), so callers don't need to null-check regardless of API version. +3. `getFormattedQueryRecord` currently ignores `record.getTokens()` entirely. The current `toString()` hack papers over this by injecting `tokenizedData: {}` into the serialized JSON string — but a caller doing `queryResponse.getFields().get(0).get("tokenizedData")` still gets `null`. -Currently `tokenizedData` is only injected inside `toString()` as a hack — it is not a real field on the object. A caller accessing the query result programmatically cannot retrieve tokenized data. +**Fix:** In `getFormattedQueryRecord`, populate `tokenizedData` from `record.getTokens()` (empty map when absent): -**Fix:** Add `tokenizedData` as a proper field, populated during construction from the API response data. Default to an empty map when absent. Expose via `getTokenizedData()`. Remove the manual injection from `toString()`. +```java +private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { + HashMap queryRecord = new HashMap<>(); + record.getFields().ifPresent(queryRecord::putAll); + queryRecord.put("tokenizedData", record.getTokens().orElse(new HashMap<>())); + return queryRecord; +} +``` -The `toString()` override is simplified to use `serializeNulls` Gson directly on the object. +Remove the manual `tokenizedData` injection hack from `QueryResponse.toString()`. The `toString()` override simplifies to standard Gson serialization with `serializeNulls`. --- From 850eedbb3454f88bfcda7d901d0a968e96182a73 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 23:24:39 +0530 Subject: [PATCH 03/48] docs: expand reasoning in Java nomenclature cleanup spec Adds detailed rationale to each design section: why the naming convention matters, why the fallback strategy was chosen over a hard cut, why skyflow_id normalization is inconsistent today, the tokenizedData API schema vs docs discrepancy, and why getErrors() is missing only from QueryResponse. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...-05-13-java-nomenclature-cleanup-design.md | 98 +++++++++++++------ 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md index 06840515..2e5e05a5 100644 --- a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md +++ b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md @@ -15,7 +15,7 @@ | HIGH | `keyID` → `keyId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | | HIGH | `tokenURI` → `tokenUri` in credentials JSON parsing (with fallback) | `BearerToken.java` | | MEDIUM | `skyflow_id` → `skyflowId` key in Get and Query response maps | `VaultController.java` | -| MEDIUM | `tokenizedData` as a real field on `QueryResponse` (always present) | `QueryResponse.java` | +| MEDIUM | `tokenizedData` per-record in QueryResponse (always present, even if empty) | `VaultController.java`, `QueryResponse.java` | | MEDIUM | `getErrors()` added to `QueryResponse` (field was missing entirely) | `QueryResponse.java` | | LOW | Audit all builder setter/getter names — confirm no `setFooID()` pattern | `VaultConfig.java`, request builders | @@ -23,15 +23,21 @@ ## Detailed design -### HIGH: Credentials JSON field renames +### HIGH: Credentials JSON field renames (`clientID` → `clientId`, `keyID` → `keyId`, `tokenURI` → `tokenUri`) -**Affected:** `BearerToken.java` (method `getBearerTokenFromCredentials`), -`SignedDataTokens.java` (method `generateSignedTokensFromCredentials`) +**Affected:** `BearerToken.java` (`getBearerTokenFromCredentials`), `SignedDataTokens.java` (`generateSignedTokensFromCredentials`) -The credentials JSON file (provided by users) currently uses `clientID`, `keyID`, `tokenURI`. -The new canonical form is `clientId`, `keyId`, `tokenUri` (acronyms treated as words, per Java camelCase convention). +**Why this change is needed:** -**Strategy:** Try the new key first; fall back to the old key if null. This allows existing credentials files to keep working during migration. +Java's naming convention treats acronyms as ordinary word components in camelCase identifiers — `Id` not `ID`, `Uri` not `URI`. The current field names `clientID`, `keyID`, `tokenURI` violate this by capitalising the acronym in full. This is inconsistent with the rest of the SDK (e.g. `setVaultId()`, `setClusterId()`) and breaks the "principle of least surprise" for Java developers who expect `clientId`. + +These field names are defined in the credentials JSON file that users create and pass to the SDK (either as a file path or as a credentials string). They are therefore part of the SDK's public contract — a change forces users to update their credentials files. This is a breaking change, which is why it is gated to the v3.0.0 major release. + +**Why a fallback is used instead of a hard cut:** + +A hard cut would silently break all existing integrations the moment users upgrade to v3. The try-new-first fallback gives users a transition window: credentials files with the old keys continue to work, and users can migrate at their own pace. The fallback can be removed in a future major version once the old form is fully deprecated. + +**Implementation strategy:** Try the new key first; fall back to the old key if null; throw if both are absent. ```java // clientID → clientId @@ -48,7 +54,7 @@ if (keyId == null) { throw new SkyflowException(...MissingKeyId...); } -// tokenURI → tokenUri (BearerToken only) +// tokenURI → tokenUri (BearerToken only — SignedDataTokens does not use tokenURI) JsonElement tokenUri = credentials.get("tokenUri"); if (tokenUri == null) tokenUri = credentials.get("tokenURI"); if (tokenUri == null) { @@ -56,15 +62,25 @@ if (tokenUri == null) { } ``` -Local variable names and private method parameter names updated to match (`clientId`, `keyId`, `tokenUri`). +Local variable names and private method parameter names are also updated to the new form (`clientId`, `keyId`, `tokenUri`) for internal consistency, though this has no effect on the public interface. --- -### MEDIUM: skyflow_id → skyflowId in Get and Query response maps +### MEDIUM: `skyflow_id` → `skyflowId` in Get and Query response maps **Affected:** `VaultController.java` — `getFormattedGetRecord()` and `getFormattedQueryRecord()` -Insert and Update responses already use `skyflowId`. Get and Query currently call `putAll(fieldsOpt.get())` which passes through the raw API field name `skyflow_id`. After the `putAll`, rename the key: +**Why this change is needed:** + +The Skyflow REST API returns records with a `skyflow_id` field in snake_case — this is the wire format. The Java SDK is responsible for translating the wire format into language-idiomatic representations before handing data to callers. Java is a camelCase language, and the SDK already normalises `skyflow_id` to `skyflowId` in Insert and Update responses: + +- `getFormattedBatchInsertRecord`: `insertRecord.put("skyflowId", recordObject.get("skyflow_id").getAsString())` +- `getFormattedBulkInsertRecord`: `insertRecord.put("skyflowId", record.getSkyflowId().get())` +- `getFormattedUpdateRecord`: `updateTokens.put("skyflowId", skyflowId)` + +However, `getFormattedGetRecord` and `getFormattedQueryRecord` call `putAll(fieldsOpt.get())` which passes the raw API map directly through — including `skyflow_id` in snake_case. This inconsistency means that developers who write `record.get("skyflowId")` after a Get or Query call get `null`, while the same code works after an Insert or Update. It forces callers to know which operation produced the response just to read a single field. + +**Implementation:** After `putAll`, check for the raw API key and rename it: ```java if (record.containsKey("skyflow_id")) { @@ -76,19 +92,33 @@ Applied in both `getFormattedGetRecord` and `getFormattedQueryRecord`. --- -### MEDIUM: tokenizedData always present in QueryResponse +### MEDIUM: `tokenizedData` always present per-record in QueryResponse **Affected:** `VaultController.java` (`getFormattedQueryRecord`), `QueryResponse.java` -**Why this change is valid:** +**Why this change is needed:** + +The cross-SDK spec requires that `tokenizedData` is always present on each record in a Query response, even when empty, to avoid nil-check boilerplate in caller code. -The Skyflow API docs state that the Query endpoint "can't return tokens" today. However: +**Current state (broken):** `getFormattedQueryRecord` only reads `record.getFields()` and completely ignores `record.getTokens()`. The `QueryResponse.toString()` method works around this with a hack — it manually injects `"tokenizedData": {}` into each record's JSON during serialisation: -1. The Fern-generated `V1FieldRecords` type explicitly defines a `tokens` field alongside `fields` — meaning the API contract already supports it and it may be populated in future. -2. The spec's cross-SDK requirement is that `tokenizedData` is always present per-record (even as an empty object), so callers don't need to null-check regardless of API version. -3. `getFormattedQueryRecord` currently ignores `record.getTokens()` entirely. The current `toString()` hack papers over this by injecting `tokenizedData: {}` into the serialized JSON string — but a caller doing `queryResponse.getFields().get(0).get("tokenizedData")` still gets `null`. +```java +for (JsonElement fieldElement : fieldsArray) { + fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); +} +``` -**Fix:** In `getFormattedQueryRecord`, populate `tokenizedData` from `record.getTokens()` (empty map when absent): +This means `response.toString()` includes `tokenizedData` but `response.getFields().get(0).get("tokenizedData")` returns `null`. Any caller working with the Java object (rather than deserialising the string) cannot access tokenized data at all. + +**Why tokens are relevant despite the API docs:** + +The Skyflow API documentation states that the Query endpoint "can't return tokens" currently. However: + +1. The Fern-generated `V1FieldRecords` type (auto-generated from the API spec) explicitly declares a `tokens` field alongside `fields`, proving the API contract already accommodates token data in query records. The docs may lag behind the schema, or this may be intentional forward compatibility. +2. `getFormattedGetRecord` uses the same `V1FieldRecords` type and also ignores `record.getTokens()` — a parallel gap that should be fixed consistently. +3. The spec's requirement is about SDK response-shape consistency across all operations, not about what the API returns today. Callers should be able to write uniform record-access code regardless of which operation produced the response. + +**Fix:** Read `record.getTokens()` in `getFormattedQueryRecord` and always add it to the record map under the `tokenizedData` key, defaulting to an empty map when absent: ```java private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { @@ -99,17 +129,25 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi } ``` -Remove the manual `tokenizedData` injection hack from `QueryResponse.toString()`. The `toString()` override simplifies to standard Gson serialization with `serializeNulls`. +The `toString()` hack in `QueryResponse.java` is removed. The `toString()` override simplifies to standard Gson serialisation with `serializeNulls`, since the data is now correctly in the map. --- -### MEDIUM: errors always present in QueryResponse +### MEDIUM: `getErrors()` added to `QueryResponse` **Affected:** `QueryResponse.java` -`QueryResponse` has no `errors` field or `getErrors()` method today — errors are only referenced in `toString()` as a hardcoded `null`. A caller cannot access errors programmatically. +**Why this change is needed:** -**Fix:** Add `private final ArrayList> errors` (always `null` — not converted to empty list) and a `getErrors()` accessor. Consistent with `GetResponse`, `InsertResponse`, `UpdateResponse` which all have `getErrors()` returning null when no errors. +All other response types in the SDK (`GetResponse`, `InsertResponse`, `UpdateResponse`, `FileUploadResponse`) expose a `getErrors()` method. `QueryResponse` is the only one that does not — the `errors` field is referenced only inside `toString()` as a hardcoded literal `null`: + +```java +responseObject.add("errors", null); +``` + +A caller who writes `queryResponse.getErrors()` gets a compile error because the method does not exist. This breaks the consistency contract that callers rely on when writing generic response-handling code across different vault operations. + +**Fix:** Add `private final ArrayList> errors` as a constructor field (always `null` — consistent with other response types that pass `null` when there are no errors) and expose it via `getErrors()`. The field will always be `null` for QueryResponse since the Query API does not currently model partial-error responses the same way batch insert does. This is kept as `null` rather than an empty list to stay consistent with the existing pattern across other response classes. --- @@ -117,15 +155,19 @@ Remove the manual `tokenizedData` injection hack from `QueryResponse.toString()` **Affected:** `VaultConfig.java`, `InsertRequest`, `UpdateRequest`, `GetRequest`, `DeleteRequest`, `FileUploadRequest`, `QueryRequest` -Confirm all methods follow `setFooId()` / `getFooId()` (title-case `Id`), not `setFooID()` (all-caps `ID`). +**Why this change is needed:** + +The same acronym-casing rule that applies to credentials fields applies to all Java method names. Any setter or getter using `ID` (all-caps) as a suffix — e.g. `setVaultID()`, `getSkyflowID()` — is non-idiomatic and inconsistent with Java convention. The spec item 15 calls out this as a verification task. + +From initial review, `setVaultId()` and `setClusterId()` in `VaultConfig` are already correct. A full grep audit across all request builder classes is required to confirm there are no remaining `setFooID()` / `getFooID()` methods that were missed. -From initial review: `setVaultId()`, `setClusterId()` in `VaultConfig` are already correct. Full grep audit required to confirm no remaining violations. +**Outcome:** If any violations are found, rename them to `setFooId()` / `getFooId()`. If none are found, this item is closed as verified-clean. --- ## What is NOT in scope -- `UpdateRequest.getData()` map key convention (user passes `skyflow_id` to identify the record to update — this is an input key, not a response key, and is not addressed in the spec) -- Any changes to generated REST client code under `com.skyflow.generated.*` -- `SKYFLOW_CREDENTIALS` environment variable name (stays `ALL_CAPS` per OS convention) -- Validation logic changes (null insert value handling is Python-only per spec) +- **`UpdateRequest.getData()` map key**: Users currently pass `skyflow_id` (snake_case) in the data map to identify the record to update. This is an *input* key consumed by the SDK internally (`updateRequest.getData().remove("skyflow_id")`), not a response field surfaced to callers. The spec does not address this and changing it would require a separate design decision. +- **Generated REST client code** under `com.skyflow.generated.*`: These files are auto-generated by Fern from the API definition. Manual edits would be overwritten on the next regeneration. +- **`SKYFLOW_CREDENTIALS` environment variable name**: Stays `ALL_CAPS` per OS and shell convention. Only the parsed field names within the JSON value change. +- **Validation logic for null/None insert values**: The spec marks this as Python-only (item 12). Java already throws on invalid input at the API boundary. From ea9efb2c551739a974ac0e164c3f024baaab3958 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 23:29:46 +0530 Subject: [PATCH 04/48] docs: correct tokenizedData implementation rationale in spec Clarifies that ignoring record.getTokens() in getFormattedQueryRecord is intentional (Query API cannot return tokens), and that the fix is to promote the toString() hack into a real always-empty field rather than reading from the API response. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../2026-05-13-java-nomenclature-cleanup-design.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md index 2e5e05a5..c56f43f6 100644 --- a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md +++ b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md @@ -110,26 +110,24 @@ for (JsonElement fieldElement : fieldsArray) { This means `response.toString()` includes `tokenizedData` but `response.getFields().get(0).get("tokenizedData")` returns `null`. Any caller working with the Java object (rather than deserialising the string) cannot access tokenized data at all. -**Why tokens are relevant despite the API docs:** +**Why `record.getTokens()` is intentionally ignored:** -The Skyflow API documentation states that the Query endpoint "can't return tokens" currently. However: +The Skyflow API documentation explicitly states the Query endpoint cannot return tokens. Ignoring `record.getTokens()` in `getFormattedQueryRecord` is therefore a deliberate design decision — not a bug. The `toString()` hack that injects `"tokenizedData": {}` confirms this intent: the author knew tokens would never be present, so they hardcoded an always-empty field for output consistency rather than trying to surface real data from the API. -1. The Fern-generated `V1FieldRecords` type (auto-generated from the API spec) explicitly declares a `tokens` field alongside `fields`, proving the API contract already accommodates token data in query records. The docs may lag behind the schema, or this may be intentional forward compatibility. -2. `getFormattedGetRecord` uses the same `V1FieldRecords` type and also ignores `record.getTokens()` — a parallel gap that should be fixed consistently. -3. The spec's requirement is about SDK response-shape consistency across all operations, not about what the API returns today. Callers should be able to write uniform record-access code regardless of which operation produced the response. +Populating `tokenizedData` from `record.getTokens()` would be wrong — it would imply the Query operation supports token retrieval, which it does not, and would mislead future maintainers. -**Fix:** Read `record.getTokens()` in `getFormattedQueryRecord` and always add it to the record map under the `tokenizedData` key, defaulting to an empty map when absent: +**Fix:** Promote the `toString()` hack into a real field. Always write an empty map under `tokenizedData` in the record — no dependency on `record.getTokens()`: ```java private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { HashMap queryRecord = new HashMap<>(); record.getFields().ifPresent(queryRecord::putAll); - queryRecord.put("tokenizedData", record.getTokens().orElse(new HashMap<>())); + queryRecord.put("tokenizedData", new HashMap<>()); // Query API cannot return tokens; always empty return queryRecord; } ``` -The `toString()` hack in `QueryResponse.java` is removed. The `toString()` override simplifies to standard Gson serialisation with `serializeNulls`, since the data is now correctly in the map. +The `toString()` hack in `QueryResponse.java` is removed. The `toString()` override simplifies to standard Gson serialisation with `serializeNulls`, since `tokenizedData` is now a real key in each record map. --- From 54cb54bd758119d8c03aad1a62ff902349dc8993 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 23:42:21 +0530 Subject: [PATCH 05/48] docs: remove tokenizedData from scope in nomenclature cleanup spec Query API cannot return tokens; the toString() inconsistency is not worth fixing since callers have no reason to access tokenizedData programmatically on query results. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...-05-13-java-nomenclature-cleanup-design.md | 42 +------------------ 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md index c56f43f6..9873fdcf 100644 --- a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md +++ b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md @@ -15,7 +15,6 @@ | HIGH | `keyID` → `keyId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | | HIGH | `tokenURI` → `tokenUri` in credentials JSON parsing (with fallback) | `BearerToken.java` | | MEDIUM | `skyflow_id` → `skyflowId` key in Get and Query response maps | `VaultController.java` | -| MEDIUM | `tokenizedData` per-record in QueryResponse (always present, even if empty) | `VaultController.java`, `QueryResponse.java` | | MEDIUM | `getErrors()` added to `QueryResponse` (field was missing entirely) | `QueryResponse.java` | | LOW | Audit all builder setter/getter names — confirm no `setFooID()` pattern | `VaultConfig.java`, request builders | @@ -92,45 +91,6 @@ Applied in both `getFormattedGetRecord` and `getFormattedQueryRecord`. --- -### MEDIUM: `tokenizedData` always present per-record in QueryResponse - -**Affected:** `VaultController.java` (`getFormattedQueryRecord`), `QueryResponse.java` - -**Why this change is needed:** - -The cross-SDK spec requires that `tokenizedData` is always present on each record in a Query response, even when empty, to avoid nil-check boilerplate in caller code. - -**Current state (broken):** `getFormattedQueryRecord` only reads `record.getFields()` and completely ignores `record.getTokens()`. The `QueryResponse.toString()` method works around this with a hack — it manually injects `"tokenizedData": {}` into each record's JSON during serialisation: - -```java -for (JsonElement fieldElement : fieldsArray) { - fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); -} -``` - -This means `response.toString()` includes `tokenizedData` but `response.getFields().get(0).get("tokenizedData")` returns `null`. Any caller working with the Java object (rather than deserialising the string) cannot access tokenized data at all. - -**Why `record.getTokens()` is intentionally ignored:** - -The Skyflow API documentation explicitly states the Query endpoint cannot return tokens. Ignoring `record.getTokens()` in `getFormattedQueryRecord` is therefore a deliberate design decision — not a bug. The `toString()` hack that injects `"tokenizedData": {}` confirms this intent: the author knew tokens would never be present, so they hardcoded an always-empty field for output consistency rather than trying to surface real data from the API. - -Populating `tokenizedData` from `record.getTokens()` would be wrong — it would imply the Query operation supports token retrieval, which it does not, and would mislead future maintainers. - -**Fix:** Promote the `toString()` hack into a real field. Always write an empty map under `tokenizedData` in the record — no dependency on `record.getTokens()`: - -```java -private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { - HashMap queryRecord = new HashMap<>(); - record.getFields().ifPresent(queryRecord::putAll); - queryRecord.put("tokenizedData", new HashMap<>()); // Query API cannot return tokens; always empty - return queryRecord; -} -``` - -The `toString()` hack in `QueryResponse.java` is removed. The `toString()` override simplifies to standard Gson serialisation with `serializeNulls`, since `tokenizedData` is now a real key in each record map. - ---- - ### MEDIUM: `getErrors()` added to `QueryResponse` **Affected:** `QueryResponse.java` @@ -165,6 +125,8 @@ From initial review, `setVaultId()` and `setClusterId()` in `VaultConfig` are al ## What is NOT in scope +- **`tokenizedData` in QueryResponse:** The Skyflow Query API explicitly cannot return tokens. The existing `toString()` hack that injects `tokenizedData: {}` is a minor inconsistency between string output and programmatic access, but since callers have no reason to access tokenized data from a query result, this is not worth fixing now. + - **`UpdateRequest.getData()` map key**: Users currently pass `skyflow_id` (snake_case) in the data map to identify the record to update. This is an *input* key consumed by the SDK internally (`updateRequest.getData().remove("skyflow_id")`), not a response field surfaced to callers. The spec does not address this and changing it would require a separate design decision. - **Generated REST client code** under `com.skyflow.generated.*`: These files are auto-generated by Fern from the API definition. Manual edits would be overwritten on the next regeneration. - **`SKYFLOW_CREDENTIALS` environment variable name**: Stays `ALL_CAPS` per OS and shell convention. Only the parsed field names within the JSON value change. From 5e484d975d3dd715c38314d83db02bd4a2316c1d Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Wed, 13 May 2026 23:47:28 +0530 Subject: [PATCH 06/48] docs: add implementation plan for Java SDK nomenclature cleanup 5-task TDD plan covering credential field renames with fallback, skyflow_id normalisation in Get/Query responses, and QueryResponse getErrors() accessor. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../2026-05-13-java-nomenclature-cleanup.md | 589 ++++++++++++++++++ 1 file changed, 589 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md diff --git a/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md b/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md new file mode 100644 index 00000000..a386accd --- /dev/null +++ b/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md @@ -0,0 +1,589 @@ +# Java SDK Nomenclature Cleanup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename credential JSON field keys (`clientID`→`clientId`, `keyID`→`keyId`, `tokenURI`→`tokenUri`) with fallback support, normalise `skyflow_id`→`skyflowId` in Get and Query responses, and add `getErrors()` to `QueryResponse`. + +**Architecture:** Three independent, targeted changes to existing files — no new files, no new abstractions. Each change is a surgical edit to one method or class, verified by unit tests that already exist or that we add inline. + +**Tech Stack:** Java 11+, JUnit 4, Mockito/PowerMock, Maven (`mvn test`) + +**Design spec:** `docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md` + +--- + +## File Map + +| File | Change | +|---|---| +| `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java` | Fallback lookup for `clientId`/`keyId`/`tokenUri` in `getBearerTokenFromCredentials` | +| `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java` | Fallback lookup for `clientId`/`keyId` in `generateSignedTokensFromCredentials` | +| `src/main/java/com/skyflow/vault/controller/VaultController.java` | Rename `skyflow_id`→`skyflowId` in `getFormattedGetRecord` and `getFormattedQueryRecord` | +| `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `errors` field and `getErrors()` accessor | +| `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` | Add tests for new-form keys, old-form fallback, and missing-key errors | +| `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` | Add tests for new-form keys, old-form fallback, and missing-key errors | +| `src/test/java/com/skyflow/vault/data/QueryResponseTest.java` | New file — tests for `getErrors()` always returning null | + +--- + +## Task 1: Credential field renames in BearerToken — new key form + +**Files:** +- Modify: `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java:92-145` +- Modify: `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` + +### Background +`getBearerTokenFromCredentials` parses a `JsonObject` representing the credentials file. It currently looks up `clientID`, `keyID`, and `tokenURI`. We need it to accept `clientId`, `keyId`, `tokenUri` (new canonical form) while still accepting the old keys as a fallback. + +The test at line 228 of `BearerTokenTests.java` currently uses the old keys — we need a parallel test using the new keys. + +- [ ] **Step 1: Write a failing test for new-form credential keys** + +Add this test to `BearerTokenTests.java`. It uses a credentials string with `clientId`, `keyId`, `tokenUri` (new form) and expects a `SkyflowException` with the `InvalidTokenUri` message (because the URI value is invalid — not because the keys are unrecognised). This confirms the new keys are read successfully. + +```java +@Test +public void testBearerTokenWithNewFormCredentialKeys() { + try { + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}"; + BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build(); + bearerToken.getBearerToken(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); + } +} +``` + +- [ ] **Step 2: Run the test to confirm it fails** + +```bash +mvn test -pl . -Dtest=BearerTokenTests#testBearerTokenWithNewFormCredentialKeys -q +``` + +Expected: FAIL — the test throws `MissingClientId` (because `clientId` is not found, only `clientID` is looked up). + +- [ ] **Step 3: Update `getBearerTokenFromCredentials` in `BearerToken.java`** + +Replace the three field lookups (lines 102–118) with fallback logic: + +```java +JsonElement clientId = credentials.get("clientId"); +if (clientId == null) clientId = credentials.get("clientID"); +if (clientId == null) { + LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); +} + +JsonElement keyId = credentials.get("keyId"); +if (keyId == null) keyId = credentials.get("keyID"); +if (keyId == null) { + LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); +} + +JsonElement tokenUri = credentials.get("tokenUri"); +if (tokenUri == null) tokenUri = credentials.get("tokenURI"); +if (tokenUri == null) { + LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); +} +``` + +Also update the `getSignedToken` call on line 121 to use the renamed variables: + +```java +String signedUserJWT = getSignedToken( + clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context +); +String basePath = Utils.getBaseURL(tokenUri.getAsString()); +``` + +Also update the private method signature at line 147–148 to use idiomatic parameter names (internal only, no public impact): + +```java +private static String getSignedToken( + String clientId, String keyId, String tokenUri, PrivateKey pvtKey, Object context +) { + final Date createdDate = new Date(); + final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000)); + io.jsonwebtoken.JwtBuilder builder = Jwts.builder() + .claim("iss", clientId) + .claim("key", keyId) + .claim("aud", tokenUri) + .claim("sub", clientId) + .expiration(expirationDate); + if (context != null) { + builder.claim("ctx", context); + } + return builder.signWith(pvtKey, Jwts.SIG.RS256).compact(); +} +``` + +- [ ] **Step 4: Run the new test to confirm it passes** + +```bash +mvn test -pl . -Dtest=BearerTokenTests#testBearerTokenWithNewFormCredentialKeys -q +``` + +Expected: PASS — `clientId` is found, execution reaches `InvalidTokenUri`. + +- [ ] **Step 5: Run the full BearerToken test suite to confirm no regressions** + +```bash +mvn test -pl . -Dtest=BearerTokenTests -q +``` + +Expected: All existing tests pass (old-form keys `clientID`/`keyID`/`tokenURI` still work via fallback). + +- [ ] **Step 6: Commit** + +```bash +git add src/main/java/com/skyflow/serviceaccount/util/BearerToken.java \ + src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +git commit -m "feat: accept clientId/keyId/tokenUri in BearerToken with fallback to old form" +``` + +--- + +## Task 2: Credential field renames in SignedDataTokens — new key form + +**Files:** +- Modify: `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java:92-122` +- Modify: `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` + +### Background +`generateSignedTokensFromCredentials` parses a credentials `JsonObject` and looks up `clientID` and `keyID`. Same rename as Task 1, but no `tokenURI` (SignedDataTokens does not need it). + +- [ ] **Step 1: Check what the existing SignedDataTokens test uses for credential keys** + +```bash +grep -n "clientID\|keyID\|clientId\|keyId" src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +``` + +Note the line number of any credentials string that uses `clientID`/`keyID` — you will add a parallel test using the new keys. + +- [ ] **Step 2: Write a failing test for new-form keys** + +Add this test to `SignedDataTokensTests.java`. It expects the token generation to fail at the private key parsing stage (not at the field-lookup stage), confirming `clientId` and `keyId` are successfully read: + +```java +@Test +public void testSignedDataTokensWithNewFormCredentialKeys() { + try { + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\"}"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("test-token"); + SignedDataTokens signedDataTokens = SignedDataTokens.builder() + .setCredentials(credentialsString) + .setDataTokens(dataTokens) + .build(); + signedDataTokens.getSignedDataTokens(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + // Should fail past field lookup — at private key parsing, not at MissingClientId + Assert.assertNotEquals(ErrorMessage.MissingClientId.getMessage(), e.getMessage()); + Assert.assertNotEquals(ErrorMessage.MissingKeyId.getMessage(), e.getMessage()); + } +} +``` + +- [ ] **Step 3: Run the test to confirm it fails** + +```bash +mvn test -pl . -Dtest=SignedDataTokensTests#testSignedDataTokensWithNewFormCredentialKeys -q +``` + +Expected: FAIL — throws `MissingClientId` because `clientId` is not yet recognised. + +- [ ] **Step 4: Update `generateSignedTokensFromCredentials` in `SignedDataTokens.java`** + +Replace the `clientID` and `keyID` lookups (lines 103–113) with fallback logic: + +```java +JsonElement clientId = credentials.get("clientId"); +if (clientId == null) clientId = credentials.get("clientID"); +if (clientId == null) { + LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); +} + +JsonElement keyId = credentials.get("keyId"); +if (keyId == null) keyId = credentials.get("keyID"); +if (keyId == null) { + LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); +} +``` + +Update the `getSignedToken` call on line 115 to use the renamed variables: + +```java +signedDataTokens = getSignedToken( + clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context); +``` + +Update the private method signature at line 124–125 to use idiomatic parameter names: + +```java +private static List getSignedToken( + String clientId, String keyId, PrivateKey pvtKey, + ArrayList dataTokens, Integer timeToLive, Object context +) { +``` + +And update the JWT claims inside `getSignedToken` (lines 142–143): + +```java +.claim("key", keyId) +.claim("sub", clientId) +``` + +- [ ] **Step 5: Run the new test to confirm it passes** + +```bash +mvn test -pl . -Dtest=SignedDataTokensTests#testSignedDataTokensWithNewFormCredentialKeys -q +``` + +Expected: PASS — `clientId` and `keyId` are found; exception is from private key parsing, not from missing fields. + +- [ ] **Step 6: Run the full SignedDataTokens test suite** + +```bash +mvn test -pl . -Dtest=SignedDataTokensTests -q +``` + +Expected: All existing tests pass. + +- [ ] **Step 7: Commit** + +```bash +git add src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java \ + src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +git commit -m "feat: accept clientId/keyId in SignedDataTokens with fallback to old form" +``` + +--- + +## Task 3: Normalise `skyflow_id` → `skyflowId` in Get and Query responses + +**Files:** +- Modify: `src/main/java/com/skyflow/vault/controller/VaultController.java:121-152` +- Modify: `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` + +### Background +`getFormattedGetRecord` and `getFormattedQueryRecord` call `putAll(fieldsOpt.get())` which passes through the raw API map — including the `skyflow_id` snake_case key from the wire format. Insert and Update responses already use `skyflowId`. This inconsistency means callers must know which operation produced the response in order to read the record ID. + +The test suite does not currently test the contents of Get or Query responses (no existing tests for `skyflowId` in these paths), so we add new unit tests. + +Because the actual vault API is not called in unit tests (no mock infrastructure for it in `VaultControllerTests`), we test the formatter methods indirectly by verifying the behaviour of the public `get()` and `query()` methods throw the right validation errors — and we test the formatters directly via reflection, or we add a thin package-private helper. + +The simplest approach: add package-private unit tests for the two static formatter methods directly. + +- [ ] **Step 1: Write failing tests for the formatter methods** + +Add these tests to `VaultControllerTests.java`: + +```java +import com.skyflow.generated.rest.types.V1FieldRecords; +import java.util.HashMap; +import java.util.Map; +import java.lang.reflect.Method; + +@Test +public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "abc-123"); + fields.put("name", "John"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); + Assert.assertEquals("skyflowId should be present", "abc-123", result.get("skyflowId")); + Assert.assertEquals("other fields should be preserved", "John", result.get("name")); +} + +@Test +public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "xyz-456"); + fields.put("email", "test@example.com"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); + Assert.assertEquals("skyflowId should be present", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); +} +``` + +- [ ] **Step 2: Run the tests to confirm they fail** + +```bash +mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId -q +``` + +Expected: FAIL — `skyflow_id` is present in the result, `skyflowId` is absent. + +- [ ] **Step 3: Update `getFormattedGetRecord` in `VaultController.java`** + +After the `putAll` block (after line 131), add the key rename: + +```java +private static synchronized HashMap getFormattedGetRecord(V1FieldRecords record) { + HashMap getRecord = new HashMap<>(); + + Optional> fieldsOpt = record.getFields(); + Optional> tokensOpt = record.getTokens(); + + if (fieldsOpt.isPresent()) { + getRecord.putAll(fieldsOpt.get()); + } else if (tokensOpt.isPresent()) { + getRecord.putAll(tokensOpt.get()); + } + + if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.remove("skyflow_id")); + } + + return getRecord; +} +``` + +- [ ] **Step 4: Update `getFormattedQueryRecord` in `VaultController.java`** + +After the `putAll` block (after line 150), add the key rename: + +```java +private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { + HashMap queryRecord = new HashMap<>(); + Optional> fieldsOpt = record.getFields(); + if (fieldsOpt.isPresent()) { + queryRecord.putAll(fieldsOpt.get()); + } + + if (queryRecord.containsKey("skyflow_id")) { + queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); + } + + return queryRecord; +} +``` + +- [ ] **Step 5: Run the new tests to confirm they pass** + +```bash +mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId -q +``` + +Expected: PASS. + +- [ ] **Step 6: Run the full VaultController test suite** + +```bash +mvn test -pl . -Dtest=VaultControllerTests -q +``` + +Expected: All existing tests pass. + +- [ ] **Step 7: Commit** + +```bash +git add src/main/java/com/skyflow/vault/controller/VaultController.java \ + src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +git commit -m "feat: normalise skyflow_id to skyflowId in Get and Query response maps" +``` + +--- + +## Task 4: Add `getErrors()` to `QueryResponse` + +**Files:** +- Modify: `src/main/java/com/skyflow/vault/data/QueryResponse.java` +- Create: `src/test/java/com/skyflow/vault/data/QueryResponseTest.java` + +### Background +`QueryResponse` is the only response class without a `getErrors()` method. The field is referenced in `toString()` as a hardcoded `null` literal but is not accessible programmatically. We add the field and accessor to match the pattern in `GetResponse`, `InsertResponse`, and `UpdateResponse` (all return `null` when no errors). + +- [ ] **Step 1: Write a failing test** + +Create `src/test/java/com/skyflow/vault/data/QueryResponseTest.java`: + +```java +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +public class QueryResponseTest { + + @Test + public void testGetErrorsReturnsNull() { + ArrayList> fields = new ArrayList<>(); + HashMap record = new HashMap<>(); + record.put("skyflowId", "abc-123"); + fields.add(record); + + QueryResponse response = new QueryResponse(fields); + + Assert.assertNull("getErrors() should return null when no errors", response.getErrors()); + } + + @Test + public void testGetErrorsIsPresentInToString() { + QueryResponse response = new QueryResponse(new ArrayList<>()); + String json = response.toString(); + Assert.assertTrue("toString() should include errors field", json.contains("\"errors\"")); + } +} +``` + +- [ ] **Step 2: Run the tests to confirm they fail** + +```bash +mvn test -pl . -Dtest=QueryResponseTest -q +``` + +Expected: FAIL — compile error: `getErrors()` method does not exist on `QueryResponse`. + +- [ ] **Step 3: Update `QueryResponse.java`** + +Add the `errors` field and accessor. The `toString()` no longer needs to manually inject `errors` since `serializeNulls` will include it automatically: + +```java +package com.skyflow.vault.data; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.HashMap; + +public class QueryResponse { + private final ArrayList> fields; + private final ArrayList> errors; + + public QueryResponse(ArrayList> fields) { + this.fields = fields; + this.errors = null; + } + + public ArrayList> getFields() { + return fields; + } + + public ArrayList> getErrors() { + return errors; + } + + @Override + public String toString() { + Gson gson = new GsonBuilder().serializeNulls().create(); + JsonObject responseObject = gson.toJsonTree(this).getAsJsonObject(); + // tokenizedData is intentionally not surfaced — Query API cannot return tokens + JsonArray fieldsArray = responseObject.get("fields").getAsJsonArray(); + for (JsonElement fieldElement : fieldsArray) { + fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); + } + return responseObject.toString(); + } +} +``` + +- [ ] **Step 4: Run the new tests to confirm they pass** + +```bash +mvn test -pl . -Dtest=QueryResponseTest -q +``` + +Expected: PASS. + +- [ ] **Step 5: Run the full test suite to confirm no regressions** + +```bash +mvn test -q +``` + +Expected: All tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/main/java/com/skyflow/vault/data/QueryResponse.java \ + src/test/java/com/skyflow/vault/data/QueryResponseTest.java +git commit -m "feat: add getErrors() accessor to QueryResponse" +``` + +--- + +## Task 5: LOW audit — verify no `setFooID` / `getFooID` violations + +**Files:** +- Read-only audit (no changes expected) + +### Background +The spec requires confirming that all builder setter/getter methods use `setFooId()` / `getFooId()` (title-case `Id`), not `setFooID()`. Initial review of `VaultConfig` already shows `setVaultId()` and `setClusterId()` are correct. This task confirms nothing was missed. + +- [ ] **Step 1: Run the grep audit** + +```bash +grep -rn "set[A-Za-z]*ID\b\|get[A-Za-z]*ID\b" \ + src/main/java/com/skyflow/config/ \ + src/main/java/com/skyflow/vault/data/ \ + src/main/java/com/skyflow/serviceaccount/ \ + --include="*.java" +``` + +Expected output: **no results** — all methods already use title-case `Id`. + +- [ ] **Step 2: If violations are found, rename them** + +For each violation (e.g. `setVaultID` → `setVaultId`), use your editor's rename refactor across all callers, then run: + +```bash +mvn test -q +``` + +Expected: All tests pass. + +- [ ] **Step 3: Commit (only if changes were made)** + +```bash +git add -p +git commit -m "fix: rename setFooID/getFooID to setFooId/getFooId per Java convention" +``` + +If no violations were found, record the result: + +```bash +git commit --allow-empty -m "chore: audit confirms no setFooID/getFooID violations in public API" +``` + +--- + +## Final verification + +- [ ] **Run the complete test suite one last time** + +```bash +mvn test -q +``` + +Expected: All tests pass with no failures or errors. From 96e7e39d6b44d5e47895d444a0d0d3ca82dba4ba Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 00:03:26 +0530 Subject: [PATCH 07/48] feat: accept clientId/keyId/tokenUri in BearerToken with fallback to old form Add fallback lookup logic so getBearerTokenFromCredentials tries new camelCase keys (clientId, keyId, tokenUri) first and falls back to the legacy all-caps forms (clientID, keyID, tokenURI) for backward compatibility during migration. Add testBearerTokenWithNewFormCredentialKeys to verify the new key form is recognized end-to-end. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../serviceaccount/util/BearerToken.java | 36 ++++++++++++------- .../serviceaccount/util/BearerTokenTests.java | 15 ++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java index 9e3a6d63..4190f1de 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +++ b/src/main/java/com/skyflow/serviceaccount/util/BearerToken.java @@ -99,30 +99,40 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage()); } - JsonElement clientID = credentials.get("clientID"); - if (clientID == null) { + // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); } - JsonElement keyID = credentials.get("keyID"); - if (keyID == null) { + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); } - JsonElement tokenURI = credentials.get("tokenURI"); - if (tokenURI == null) { + JsonElement tokenUri = credentials.get("tokenUri"); + if (tokenUri == null) { + tokenUri = credentials.get("tokenURI"); + } + if (tokenUri == null) { LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); } PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); String signedUserJWT = getSignedToken( - clientID.getAsString(), keyID.getAsString(), tokenURI.getAsString(), pvtKey, context + clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context ); - String basePath = Utils.getBaseURL(tokenURI.getAsString()); + String basePath = Utils.getBaseURL(tokenUri.getAsString()); API_CLIENT_BUILDER.url(basePath); ApiClient apiClient = API_CLIENT_BUILDER.token("token").build(); AuthenticationClient authenticationApi = apiClient.authentication(); @@ -145,15 +155,15 @@ private static V1GetAuthTokenResponse getBearerTokenFromCredentials( } private static String getSignedToken( - String clientID, String keyID, String tokenURI, PrivateKey pvtKey, Object context + String clientId, String keyId, String tokenUri, PrivateKey pvtKey, Object context ) { final Date createdDate = new Date(); final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000)); io.jsonwebtoken.JwtBuilder builder = Jwts.builder() - .claim("iss", clientID) - .claim("key", keyID) - .claim("aud", tokenURI) - .claim("sub", clientID) + .claim("iss", clientId) + .claim("key", keyId) + .claim("aud", tokenUri) + .claim("sub", clientId) .expiration(expirationDate); if (context != null) { diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java index ecd38e84..77a1810a 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java @@ -249,4 +249,19 @@ public void testInvalidTokenURIInCredentialsForCredentials() throws SkyflowExcep Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); } } + + @Test + public void testBearerTokenWithNewFormCredentialKeys() { + try { + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLp0TVwidRMtZ\\n4tGLHPDEF6ihmE4OHSR/r5rZGqE+PNtw/uwXzBrfz1Mktb0hddMZNwC2IKhHE0Yw\\nvtBT0jsfy4OUQR13Mohn9znz+5TES/yXjkvZjhZKzs5rxNw/cO8lpKYUYdwbFzwl\\n9e3joCsWBXBDCbXdLQGPyggJV+KBI0LBal+LngNLU/U680LRlybCKCTyyrF0SERD\\npytcpnq41CS2Q0ZDfkK/zLrvsCkEBU8xYeAf/TphXMKeqvMGTqxxg6IPOKfYya7Q\\nnH9eZ1pn1SCe6N5XBUpQpB4K+1IZKvadOYpYWzRgM+tT5k4UVsg6s7kUm8k9n85/\\nNQMjMY2XAgMBAAECggEASlg05ClgcaBxn0H1H3tKipImbaX7/O8qjbAW162s6V3m\\nzuN2ogkVvXcQUFL3vkJc7EFeEjNKnvLoVKFXXvADiBWw6np591MINdrmOM1R1ICS\\ntW9dGU9TAIb+LsjneYsqLrw6DIruAG+LjVSU97UlK2XmRmppAvQBid+Rpg7I9Dsy\\naJyGjDHeC3RyYYNfpei2dBPUYlUjOkBqgYGOOyjYxHzzgYtdVZku0JPtsAey3WKL\\nSbu8ryugu7r23fxP50H3FtYz91TPlVu1zVEk9Viizp2c9642ZKEoA0bB/bSNMUnt\\nZ/kemZENAzC7tnoYgwN09rI3h0+U5jaU1BhXbrLpAQKBgQDt8eaywv6j+Hdv8i7S\\nyMnZE4CaM70Z319ctJPlt2QdCZp8dtac858qnnrrZSCWV3n3yMv//bf1WZB4Lssw\\nuxBzSCFI/imG6eY9uQA6yXLl1TY9DA5IJ8s2LGzwmtA1q+vC+jzWs+0+S/evUewo\\nTZGQuNjHMHoM22jeLErqQZkHUQKBgQDAxz1WY56ZHdC3Y4aXkDeb5Ag+ZJV8Uqwn\\nootA2zHCaEx8gM9CzChCl4pQcghHFXv4eEKqezdWSK+SIRA1CtR+q8g5dP8YtAkR\\n9Uav6/fEkM8iCUvhZg+1DPRShu15nQF0ZAleSJ9OiSW5pIfAbY79RHru8H31azhE\\nDOWezXbcZwKBgB9LAAckg+62n6aWWDgadglZekFNaqI7cUQ073p3mvACslGKI4Fy\\nvM0TGKFapGWBTaYbv1CEYqwewlQ7+zcGcwxmQRJjcryuiDw312Lj2XuGheKTclFl\\nAmG2iAFAqv9UA+aZmGS4NwxJW2KwSHmocetxk/jmVDbaqDkH5DZYuDJxAoGBAJqn\\n/PRujVEnk0dc6CB1ybcd9OMhTK/ln0lY5MDOWRgvFpWXvS9InE/4RTWOlkd42/EV\\ngd5FZbqqK3hfYCI9owZQiBxYWUMXRGOM0/3Un/ypdBNJQ//7IkTMtMH0j1XOeNlI\\nXB+wwWV/L63EakgdXOag5sMEWvjl4MjvU9PX4DCnAoGAR0c567DWbkTXvcNIjvNF\\nNK8suq/fGt4dpbkkFOEHjgqFd5RsjFHKc98JVrudPweUR7YjpeKQaeNKXfVFd4+N\\nDPOs0zWSsaHckh1g9djkZlidha9SD/V6cOpxi3g2okcn/LI7h8NyNlAwDSn2mPEi\\nMd3mrgMCZwJsXLndGQSDVUw=\\n-----END PRIVATE KEY-----\\n\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}"; + BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build(); + bearerToken.getBearerToken(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + // InvalidTokenUri means new-form keys were resolved successfully — failure is at URL parsing, not field lookup + Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); + } + } } From c46ced78ed33e89d7a029fc961a12840e6b48256 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 00:37:43 +0530 Subject: [PATCH 08/48] feat: accept clientId/keyId in SignedDataTokens with fallback to old form Add fallback logic to GenerateSignedTokensFromCredentials so both new-form keys (clientId/keyId) and legacy all-caps keys (clientID/keyID) are accepted during migration. Mirrors the pattern already applied to BearerToken.java. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../serviceaccount/util/SignedDataTokens.java | 23 ++++++++++++------- .../util/SignedDataTokensTests.java | 20 ++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java index 0ce14007..b2c28ea1 100644 --- a/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +++ b/src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java @@ -100,20 +100,27 @@ private static List generateSignedTokensFromCredentials throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingPrivateKey.getMessage()); } - JsonElement clientID = credentials.get("clientID"); - if (clientID == null) { + // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); } - JsonElement keyID = credentials.get("keyID"); - if (keyID == null) { + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); } PrivateKey pvtKey = Utils.getPrivateKeyFromPem(privateKey.getAsString()); signedDataTokens = getSignedToken( - clientID.getAsString(), keyID.getAsString(), pvtKey, dataTokens, timeToLive, context); + clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context); } catch (RuntimeException e) { LogUtil.printErrorLog(ErrorLogs.SIGNED_DATA_TOKENS_REJECTED.getLog()); throw new SkyflowException(e); @@ -122,7 +129,7 @@ private static List generateSignedTokensFromCredentials } private static List getSignedToken( - String clientID, String keyID, PrivateKey pvtKey, + String clientId, String keyId, PrivateKey pvtKey, ArrayList dataTokens, Integer timeToLive, Object context ) { final Date createdDate = new Date(); @@ -139,8 +146,8 @@ private static List getSignedToken( io.jsonwebtoken.JwtBuilder builder = Jwts.builder() .claim("iss", "sdk") .claim("iat", (createdDate.getTime() / 1000)) - .claim("key", keyID) - .claim("sub", clientID) + .claim("key", keyId) + .claim("sub", clientId) .claim("tok", dataToken) .expiration(expirationDate); diff --git a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java index 5e2cbe60..93d69b0e 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java @@ -221,6 +221,26 @@ public void testInvalidKeySpecInCredentials() { } } + @Test + public void testSignedDataTokensWithNewFormCredentialKeys() { + try { + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\"}"; + ArrayList dataTokens = new ArrayList<>(); + dataTokens.add("test-token"); + SignedDataTokens signedDataTokens = SignedDataTokens.builder() + .setCredentials(credentialsString) + .setDataTokens(dataTokens) + .build(); + signedDataTokens.getSignedDataTokens(); + Assert.fail(EXCEPTION_NOT_THROWN); + } catch (SkyflowException e) { + // Failure at RSA key parsing (not field lookup) confirms new-form keys clientId/keyId were found + Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); + Assert.assertEquals(ErrorMessage.InvalidKeySpec.getMessage(), e.getMessage()); + } + } + @Test public void testSignedDataTokenResponse() { try { From 16e6ef5b5d0529ed121d2a6a089d687bd70698d5 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 00:45:26 +0530 Subject: [PATCH 09/48] feat: normalise skyflow_id to skyflowId in Get and Query response maps Insert and Update responses already used camelCase skyflowId; Get and Query were passing through the raw wire-format snake_case key. Add the rename in getFormattedGetRecord and getFormattedQueryRecord, and add reflection-based unit tests to cover both formatters. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../vault/controller/VaultController.java | 10 ++++ .../controller/VaultControllerTests.java | 57 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java index acac9608..30b6ec49 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -129,6 +129,11 @@ private static synchronized HashMap getFormattedGetRecord(V1Fiel } else if (tokensOpt.isPresent()) { getRecord.putAll(tokensOpt.get()); } + + if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.remove("skyflow_id")); + } + return getRecord; } @@ -148,6 +153,11 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi if (fieldsOpt.isPresent()) { queryRecord.putAll(fieldsOpt.get()); } + + if (queryRecord.containsKey("skyflow_id")) { + queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); + } + return queryRecord; } diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 4115bc2c..2c2e8995 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -10,6 +10,7 @@ import com.skyflow.errors.HttpStatus; import com.skyflow.errors.SkyflowException; import com.skyflow.generated.rest.ApiClient; +import com.skyflow.generated.rest.types.V1FieldRecords; import com.skyflow.utils.Constants; import com.skyflow.utils.Utils; import com.skyflow.vault.data.*; @@ -19,6 +20,10 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + public class VaultControllerTests { 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"; @@ -185,4 +190,56 @@ public void testInvalidRequestInFileUploadMethod() { } } + @Test + public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "abc-123"); + fields.put("name", "John"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); + Assert.assertEquals("skyflowId should be present", "abc-123", result.get("skyflowId")); + Assert.assertEquals("other fields should be preserved", "John", result.get("name")); + } + + @Test + public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "xyz-456"); + fields.put("email", "test@example.com"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); + Assert.assertEquals("skyflowId should be present", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); + } + + @Test + public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception { + // tokens branch: fields absent, tokens present + Map tokens = new HashMap<>(); + tokens.put("skyflow_id", "tok-789"); + tokens.put("card_number", "tok-card-abc"); + V1FieldRecords record = V1FieldRecords.builder().tokens(tokens).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); + Assert.assertEquals("skyflowId should be present", "tok-789", result.get("skyflowId")); + Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); + } + } From 77d24244796ce671ca6833ed4451f82275f85c08 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 09:14:15 +0530 Subject: [PATCH 10/48] feat: add getErrors() accessor to QueryResponse Adds a private final errors field (always null) and its public accessor to QueryResponse, matching the pattern in GetResponse and InsertResponse. Removes the hardcoded responseObject.add("errors", null) from toString() since serializeNulls on the declared field handles it automatically. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../com/skyflow/vault/data/QueryResponse.java | 20 ++++++++++--- .../skyflow/vault/data/QueryResponseTest.java | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/skyflow/vault/data/QueryResponseTest.java diff --git a/src/main/java/com/skyflow/vault/data/QueryResponse.java b/src/main/java/com/skyflow/vault/data/QueryResponse.java index 7a1bca51..9a6b6804 100644 --- a/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ b/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -1,32 +1,44 @@ package com.skyflow.vault.data; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import java.util.ArrayList; import java.util.HashMap; public class QueryResponse { private final ArrayList> fields; - private ArrayList> tokenizedData; + private final ArrayList> errors; public QueryResponse(ArrayList> fields) { this.fields = fields; + this.errors = null; } 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 GsonBuilder().serializeNulls().create(); JsonObject responseObject = gson.toJsonTree(this).getAsJsonObject(); JsonArray fieldsArray = responseObject.get("fields").getAsJsonArray(); + // tokenizedData is intentionally injected per-record — Query API cannot return tokens; + // this ensures the field is always present in serialised output for cross-SDK consistency for (JsonElement fieldElement : fieldsArray) { fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); } - responseObject.add("errors", null); - responseObject.remove("tokenizedData"); return responseObject.toString(); } } diff --git a/src/test/java/com/skyflow/vault/data/QueryResponseTest.java b/src/test/java/com/skyflow/vault/data/QueryResponseTest.java new file mode 100644 index 00000000..428b1151 --- /dev/null +++ b/src/test/java/com/skyflow/vault/data/QueryResponseTest.java @@ -0,0 +1,29 @@ +package com.skyflow.vault.data; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +public class QueryResponseTest { + + @Test + public void testGetErrorsReturnsNull() { + ArrayList> fields = new ArrayList<>(); + HashMap record = new HashMap<>(); + record.put("skyflowId", "abc-123"); + fields.add(record); + + QueryResponse response = new QueryResponse(fields); + + Assert.assertNull("getErrors() should return null when no errors", response.getErrors()); + } + + @Test + public void testGetErrorsIsPresentInToString() { + QueryResponse response = new QueryResponse(new ArrayList<>()); + String json = response.toString(); + Assert.assertTrue("toString() should include errors:null", json.contains("\"errors\":null")); + } +} From 54b500ad8bd671f63c112c6e14b80b115a559811 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 09:17:38 +0530 Subject: [PATCH 11/48] chore: audit confirms no setFooID/getFooID violations in public API From 33d6c22bd0171884545130b7e78f8f58f8a199fb Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 09:26:32 +0530 Subject: [PATCH 12/48] chore: add Claude Code setup (CLAUDE.md + .claude/) Adapted from skyflow-node PR #305. Includes: - CLAUDE.md with project overview, structure, naming conventions, build commands - .claude/settings.json with PostToolUse compile+checkstyle hooks, PreToolUse generated-code guard, Stop notification; paths are relative (no hardcoded user dirs) - .claude/commands/: code-review, code-security, sdk-sample, test slash commands Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/commands/code-review.md | 65 ++++++++++++++++++++++ .claude/commands/code-security.md | 58 ++++++++++++++++++++ .claude/commands/sdk-sample.md | 35 ++++++++++++ .claude/commands/test.md | 62 +++++++++++++++++++++ .claude/settings.json | 56 +++++++++++++++++++ CLAUDE.md | 91 +++++++++++++++++++++++++++++++ 6 files changed, 367 insertions(+) create mode 100644 .claude/commands/code-review.md create mode 100644 .claude/commands/code-security.md create mode 100644 .claude/commands/sdk-sample.md create mode 100644 .claude/commands/test.md create mode 100644 .claude/settings.json create mode 100644 CLAUDE.md diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md new file mode 100644 index 00000000..a6671b18 --- /dev/null +++ b/.claude/commands/code-review.md @@ -0,0 +1,65 @@ +You are a senior engineer performing a thorough code review on the Skyflow Java SDK. + +## Review Mode + +Use `$ARGUMENTS` to determine scope: +- `full review` — scan all files under `src/main/java/com/skyflow/` recursively (exclude `generated/`) +- A file or directory path — review only that path +- Empty / default — review files changed on current branch vs `main`: + ```bash + git diff main...HEAD --name-only | grep '\.java$' | grep -v 'generated' + ``` + +## What to Review + +**Skip entirely:** `src/main/java/com/skyflow/generated/` — Fern-generated REST client, read-only. + +### 1. Request / Response / Options patterns +- Request builders must validate required fields in `build()` and throw `SkyflowException` with `ErrorCode.INVALID_INPUT` +- Response objects must expose typed getters — no raw `HashMap` returned without a getter +- All response classes must have `getErrors()` returning `null` (not absent) when no errors + +### 2. Error handling +- All public methods must declare `throws SkyflowException` +- `SkyflowException` must be thrown (not swallowed) on invalid input +- No `System.out.println` or bare `e.printStackTrace()` — use `LogUtil` +- Catch blocks must not silently drop exceptions + +### 3. Naming conventions +- Classes: `PascalCase` +- Methods / fields: `camelCase` — acronyms as words: `skyflowId` not `skyflowID`, `tokenUri` not `tokenURI` +- Constants: `UPPER_SNAKE_CASE` +- Builder methods: `setFooId()` not `setFooID()` + +### 4. Response field normalisation +- All response maps must use `skyflowId` (camelCase), never `skyflow_id` (snake_case) +- `getErrors()` must be present on every response class + +### 5. Test coverage +- Every public method must have at least one positive and one negative test +- Tests must use `Assert.assertEquals` / `Assert.assertNull` — not just `Assert.fail` guards +- No mocking of the production class under test + +### 6. Code quality +- No magic strings — use `Constants` or `ErrorMessage` enums +- No duplicate validation logic across request classes +- Methods over 40 lines are a smell — flag for decomposition +- No `@SuppressWarnings` without a comment explaining why + +## Output Format + +Group findings by file. For each file: + +``` +### path/to/File.java + +| Severity | Line | Finding | +|---|---|---| +| Critical | 42 | SkyflowException swallowed in catch block | +| Bug | 87 | skyflow_id not normalised to skyflowId | +| Quality | 103 | Magic string "records" — use Constants | +``` + +Severities: **Critical** (data loss / silent failure) | **Bug** (wrong behaviour) | **Edge Case** (unhandled input) | **Quality** (maintainability) | **Smell** (minor style) + +End with a tech-debt summary table and a verdict: `APPROVE` / `APPROVE WITH FIXES` / `REQUEST CHANGES`. diff --git a/.claude/commands/code-security.md b/.claude/commands/code-security.md new file mode 100644 index 00000000..7a2ffcf6 --- /dev/null +++ b/.claude/commands/code-security.md @@ -0,0 +1,58 @@ +You are a security engineer auditing the Skyflow Java SDK for vulnerabilities. + +## Audit Scope + +Use `$ARGUMENTS` to determine target files. If none provided, run: +```bash +git diff main...HEAD --name-only | grep '\.java$' | grep -v 'generated' +``` + +**Skip:** `src/main/java/com/skyflow/generated/` — observations only, no edits. + +## Security Checks + +### 1. Credential and token exposure (Critical) +- Bearer tokens, API keys, and private keys must never appear in logs, error messages, exception messages, or `toString()` output +- `Credentials` fields (`path`, `token`, `apiKey`, `credentialsString`) must not be serialised to logs +- JWT claims must not be logged + +### 2. Input validation (High) +- All string inputs from callers must be null/empty checked before use +- File paths passed to `new File(path)` must not allow path traversal (`../`) +- JSON strings parsed with `JsonParser` must be wrapped in try/catch for `JsonSyntaxException` + +### 3. Credentials file handling (High) +- Credentials files must only be read from paths provided by the caller — no environment variable path injection without sanitisation +- `FileReader` must be in a try-with-resources or explicitly closed + +### 4. HTTP security (Medium) +- All API calls must go over HTTPS — verify `Utils.getBaseURL` enforces this +- Authorization headers must not be logged at any log level +- HTTP timeouts must be configured + +### 5. Error information leakage (Medium) +- `SkyflowException` messages must not include raw server response bodies that could contain PII +- Stack traces must not be surfaced to callers — wrap in `SkyflowException` + +### 6. Dependency vulnerabilities (Low) +- Note any dependencies that are known to have CVEs (check pom.xml versions) + +### 7. Authentication lifecycle (Medium) +- Bearer token caching must check expiry before reuse +- Token refresh must be thread-safe (`synchronized` or equivalent) + +## Output Format + +For each finding: + +``` +### path/to/File.java : line N + +**Severity:** Critical / High / Medium / Low / Info +**Risk:** What an attacker could do +**Trigger:** Input or code path that triggers the vulnerability +**Fix:** Concrete remediation with code example +**CWE:** CWE-NNN +``` + +End with a summary table and overall risk rating. diff --git a/.claude/commands/sdk-sample.md b/.claude/commands/sdk-sample.md new file mode 100644 index 00000000..f984878e --- /dev/null +++ b/.claude/commands/sdk-sample.md @@ -0,0 +1,35 @@ +Create a Skyflow Java SDK sample file demonstrating: $ARGUMENTS + +## Requirements + +### File placement +- Create under `samples/src/main/java/com/example//` +- Name: `Example.java` +- Package: `com.example.` + +### Structure (follow this order) +1. Package declaration +2. Imports — only from `com.skyflow.*`, `java.*`; never from `com.skyflow.generated.*` +3. Public class with `main(String[] args) throws Exception` +4. Credentials setup using `Credentials` with `setPath()` pointing to `"credentials.json"` +5. `VaultConfig` with `setVaultId`, `setClusterId`, `setEnv(Env.PROD)` +6. `Skyflow` client via `Skyflow.builder().addVaultConfig(vaultConfig).build()` +7. Request object built via the appropriate `*Request.builder()` pattern +8. Options object if applicable (e.g. `InsertOptions`) +9. Call the vault method inside a try/catch for `SkyflowException` +10. Print the response using `System.out.println(response)` + +### Rules +- All vault IDs / cluster IDs use placeholder strings: `""`, `""` +- Credentials file path: `"credentials.json"` (relative — do not hardcode absolute paths) +- Always catch `SkyflowException` and print `e.getMessage()` +- Keep under 80 lines +- No business logic — just the minimal SDK usage pattern + +### After creating the file +Run a compile check: +```bash +cd samples && mvn compile -q 2>&1 | tail -20 +``` + +Report the file path and any compile errors. diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 00000000..7e7bdcb2 --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,62 @@ +Run the Skyflow Java SDK quality pipeline. + +Use `$ARGUMENTS` to target a specific test class (e.g. `BearerTokenTests`). If empty, run the full suite. + +## Pipeline + +### Step 1 — Compile +```bash +mvn compile -q 2>&1 | tail -20 +``` +Expected: no output (clean compile). Report any errors. + +### Step 2 — Checkstyle +```bash +mvn checkstyle:check -q 2>&1 | tail -20 +``` +Expected: BUILD SUCCESS. Report any violations (excludes `generated/` per pom.xml config). + +### Step 3 — Build +```bash +mvn package -DskipTests -q 2>&1 | tail -20 +``` +Expected: BUILD SUCCESS. + +### Step 4 — Tests +If `$ARGUMENTS` is set: +```bash +mvn test -Dtest=$ARGUMENTS -q 2>&1 | tail -40 +``` +Otherwise: +```bash +mvn test -q 2>&1 | tail -40 +``` +Report: tests run, failures, errors. Flag any pre-existing failures separately from new ones. + +### Step 5 — Coverage analysis +Flag any public interface class (`src/main/java/com/skyflow/vault/`, `src/main/java/com/skyflow/config/`, `src/main/java/com/skyflow/serviceaccount/`) that has no corresponding test file under `src/test/`. + +For classes that do have tests, check whether each public method has at least one positive and one negative test case. List any gaps. + +### Step 6 — Edge case identification +For any test class below complete coverage, identify missing scenarios: +- Null / empty inputs +- Invalid types / wrong enum values +- Concurrent / reuse scenarios +- Error paths (API rejection, network failure) + +Write concrete JUnit 4 test method stubs (not full implementations) for each gap. + +### Step 7 — Report + +``` +| Step | Status | Notes | +|---|---|---| +| Compile | ✅ / ❌ | ... | +| Checkstyle | ✅ / ❌ | ... | +| Build | ✅ / ❌ | ... | +| Tests | ✅ / ❌ | N passed, M failed | +| Coverage gaps | ... | list classes | +``` + +Conclude with **READY TO MERGE** or **NEEDS FIXES** and a prioritised fix list. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..be1f09bc --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,56 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n subprocess.run(['mvn', 'checkstyle:check', '-q'], capture_output=True)\n\"" + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n r = subprocess.run(['mvn', 'compile', '-q'], capture_output=True, text=True)\n out = (r.stdout + r.stderr).strip()\n if out:\n lines = out.splitlines()\n print('\\n'.join(lines[-20:]))\n\"" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',d.get('file_path','')); banned='generated'; (sys.stderr.write('BLOCKED: Fern-generated code — do not edit manually\\n'), sys.exit(2)) if banned in p and 'src/main/java/com/skyflow/generated' in p else sys.exit(0)\"" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || true" + } + ] + } + ] + }, + "permissions": { + "allow": [ + "Bash(mvn *)", + "Bash(java *)", + "Bash(python3 *)" + ], + "deny": [ + "Edit(src/main/java/com/skyflow/generated/**)", + "Write(src/main/java/com/skyflow/generated/**)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1c7fa068 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# Skyflow Java SDK — Claude Code Instructions + +## Project Overview + +This is the Skyflow Java SDK (`skyflow-java`). It provides a Java interface to the Skyflow Data Privacy Vault API — vault operations (insert, get, update, delete, query, tokenize, detokenize), service account authentication (bearer tokens, signed data tokens), connections, detect, and audit. + +**Current version:** 2.x (v3.0.0 in preparation — see `docs/superpowers/specs/`) + +## Critical Boundary — Generated Code + +**Never edit files under `src/main/java/com/skyflow/generated/`.** + +These are auto-generated by [Fern](https://buildwithfern.com) from the Skyflow API definition. Manual edits are overwritten on the next generation run. If you find a bug in generated code, report it — do not patch it directly. + +The `pom.xml` checkstyle and test configs already exclude `generated/` from all checks. + +## Project Structure + +``` +src/ + main/java/com/skyflow/ + config/ # VaultConfig, Credentials, ConnectionConfig + vault/ + controller/ # VaultController — core SDK logic, API call orchestration + data/ # Request/Response objects: InsertRequest, GetResponse, etc. + tokens/ # DetokenizeRequest/Response, TokenizeRequest/Response + connection/ # InvokeConnectionRequest/Response + audit/ # ListEventRequest/Response + detect/ # Deidentify/Reidentify requests/responses + serviceaccount/ + util/ # BearerToken, SignedDataTokens — credential parsing + JWT + enums/ # LogLevel, RedactionType, TokenMode, Env, etc. + errors/ # SkyflowException, ErrorCode, ErrorMessage + utils/ # Utils, Constants, HttpUtility, LogUtil + generated/ # ← FERN-GENERATED, DO NOT EDIT + test/java/com/skyflow/ + ... # JUnit 4 tests mirroring the main structure +samples/ # Standalone Maven project with usage examples +docs/ + superpowers/ + specs/ # Design specs for in-progress features + plans/ # Implementation plans +``` + +## Naming Conventions + +- **Acronyms as words:** `skyflowId` (not `skyflowID`), `clientId` (not `clientID`), `tokenUri` (not `tokenURI`), `keyId` (not `keyID`) +- **Builder setters:** `setVaultId()`, `setClusterId()`, `setSkyflowId()` — never `setVaultID()` +- **Response maps:** always use `skyflowId` (camelCase) — the raw API returns `skyflow_id` (snake_case) which VaultController normalises before returning to callers +- **Constants class:** use `com.skyflow.utils.Constants` for string literals; `ErrorMessage` enum for error message strings + +## Build and Test + +```bash +mvn compile -q # compile +mvn checkstyle:check -q # lint (config: checkstyle.xml) +mvn test -q # full test suite (JUnit 4) +mvn test -Dtest=ClassName # single test class +mvn package -DskipTests -q # build jar +``` + +Samples (separate Maven project): +```bash +cd samples && mvn compile -q +``` + +## Credentials JSON Format + +The SDK reads a `credentials.json` file for service account authentication. The canonical field names (v3+) are: + +```json +{ + "clientId": "...", + "keyId": "...", + "tokenUri": "...", + "privateKey": "..." +} +``` + +The legacy all-caps forms (`clientID`, `keyID`, `tokenURI`) are accepted as fallbacks for migration. + +## Active Work + +See `docs/superpowers/specs/` for in-progress design specs and `docs/superpowers/plans/` for implementation plans. + +## Slash Commands + +- `/code-review` — code review against SDK patterns (see `.claude/commands/code-review.md`) +- `/code-security` — security audit (see `.claude/commands/code-security.md`) +- `/sdk-sample ` — generate a sample file for a feature +- `/test [ClassName]` — run quality pipeline (compile → checkstyle → build → test → coverage) From f4d71ae0a5ce6e4df7b2dd43df8c911fa5cde5b4 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 09:54:12 +0530 Subject: [PATCH 13/48] chore: fix gaps and inaccuracies in Claude setup files - CLAUDE.md: add vault/bin/ package, all 5 controllers, pre-existing test failure baseline - settings.json: fix checkstyle hook to print violations (was silently swallowing output with capture_output=True) - sdk-sample.md: fix InsertOptions (doesn't exist), correct sample package structure, correct credential type per feature - code-review.md: fix validation location (controller not build()), fix HashMap rule (SDK pattern is raw HashMaps) - test.md: document pre-existing failures, note checkstyle failsOnError Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/commands/code-review.md | 7 ++-- .claude/commands/sdk-sample.md | 67 ++++++++++++++++++++++----------- .claude/commands/test.md | 12 +++++- .claude/settings.json | 2 +- CLAUDE.md | 19 +++++++++- 5 files changed, 79 insertions(+), 28 deletions(-) diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md index a6671b18..6bd6a69c 100644 --- a/.claude/commands/code-review.md +++ b/.claude/commands/code-review.md @@ -15,9 +15,10 @@ Use `$ARGUMENTS` to determine scope: **Skip entirely:** `src/main/java/com/skyflow/generated/` — Fern-generated REST client, read-only. ### 1. Request / Response / Options patterns -- Request builders must validate required fields in `build()` and throw `SkyflowException` with `ErrorCode.INVALID_INPUT` -- Response objects must expose typed getters — no raw `HashMap` returned without a getter -- All response classes must have `getErrors()` returning `null` (not absent) when no errors +- Request builders are plain data holders — validation happens in `Validations.validateXxxRequest()` inside the controller, not in `build()`. Flag if validation logic is duplicated outside `Validations`. +- Response getters returning `ArrayList>` is the established SDK pattern — do not flag these as violations. +- All response classes must have `getErrors()` returning `null` (not absent) when no errors. `QueryResponse` is the historical exception — it now has `getErrors()` too. +- No separate `*Options` classes exist — options are fields on the request builder itself. ### 2. Error handling - All public methods must declare `throws SkyflowException` diff --git a/.claude/commands/sdk-sample.md b/.claude/commands/sdk-sample.md index f984878e..79e702aa 100644 --- a/.claude/commands/sdk-sample.md +++ b/.claude/commands/sdk-sample.md @@ -1,33 +1,58 @@ Create a Skyflow Java SDK sample file demonstrating: $ARGUMENTS -## Requirements +## File placement -### File placement -- Create under `samples/src/main/java/com/example//` -- Name: `Example.java` -- Package: `com.example.` +| Feature type | Package | Directory | +|---|---|---| +| Vault ops (insert/get/update/delete/query/tokenize) | `com.example.vault` | `samples/src/main/java/com/example/vault/` | +| Service account auth | `com.example.serviceaccount` | `samples/src/main/java/com/example/serviceaccount/` | +| Connection | `com.example.connection` | `samples/src/main/java/com/example/connection/` | +| Detect | `com.example.detect` | `samples/src/main/java/com/example/detect/` | + +File name: `Example.java` + +## Structure (follow this order) -### Structure (follow this order) 1. Package declaration 2. Imports — only from `com.skyflow.*`, `java.*`; never from `com.skyflow.generated.*` -3. Public class with `main(String[] args) throws Exception` -4. Credentials setup using `Credentials` with `setPath()` pointing to `"credentials.json"` -5. `VaultConfig` with `setVaultId`, `setClusterId`, `setEnv(Env.PROD)` -6. `Skyflow` client via `Skyflow.builder().addVaultConfig(vaultConfig).build()` -7. Request object built via the appropriate `*Request.builder()` pattern -8. Options object if applicable (e.g. `InsertOptions`) -9. Call the vault method inside a try/catch for `SkyflowException` -10. Print the response using `System.out.println(response)` - -### Rules -- All vault IDs / cluster IDs use placeholder strings: `""`, `""` -- Credentials file path: `"credentials.json"` (relative — do not hardcode absolute paths) +3. Public class with `main(String[] args) throws SkyflowException` +4. Credentials setup — choose based on feature: + - **Vault ops:** `credentials.setApiKey("")` or `credentials.setCredentialsString("")` + - **Service account:** `credentials.setPath("credentials.json")` (path to the service account JSON file) +5. `VaultConfig` with `setVaultId`, `setClusterId`, `setEnv(Env.PROD)`, `setCredentials(credentials)` +6. Build the Skyflow client: + ```java + Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.DEBUG) + .addVaultConfig(vaultConfig) + .build(); + ``` +7. Request object via `*Request.builder()` — options go directly on the builder (no separate Options class): + ```java + // Example: InsertRequest with tokenMode + InsertRequest request = InsertRequest.builder() + .table("...") + .values(records) + .tokenMode(TokenMode.ENABLE) + .build(); + ``` +8. Call the vault method inside a try/catch for `SkyflowException`: + ```java + InsertResponse response = skyflowClient.vault().insert(request); + System.out.println(response); + ``` + +## Rules + +- Vault IDs / cluster IDs use placeholders: `""`, `""` +- Credential values use placeholders: `""`, `""` +- Credentials file path: `"credentials.json"` (relative — no absolute paths) - Always catch `SkyflowException` and print `e.getMessage()` +- No separate `*Options` classes — they don't exist in this SDK; use request builder methods - Keep under 80 lines -- No business logic — just the minimal SDK usage pattern -### After creating the file -Run a compile check: +## After creating the file + ```bash cd samples && mvn compile -q 2>&1 | tail -20 ``` diff --git a/.claude/commands/test.md b/.claude/commands/test.md index 7e7bdcb2..98397f8f 100644 --- a/.claude/commands/test.md +++ b/.claude/commands/test.md @@ -2,6 +2,16 @@ Run the Skyflow Java SDK quality pipeline. Use `$ARGUMENTS` to target a specific test class (e.g. `BearerTokenTests`). If empty, run the full suite. +## Known Pre-existing Failures (not regressions) + +Before reporting failures, check against this baseline: +- `HttpUtilityTests` — ALL tests fail (JDK 21 + PowerMock `InaccessibleObject` incompatibility) +- `TokenTests#testExpiredTokenForIsExpiredToken` — needs live credentials +- `VaultClientTests#testSetBearerTokenWithEnvCredentials` — needs `SKYFLOW_CREDENTIALS` env var +- `ConnectionClientTests#testSetBearerTokenWithEnvCredentials` — needs `SKYFLOW_CREDENTIALS` env var + +Baseline: 374 tests, ~5 failures, ~4 errors. Only report failures **beyond** this baseline. + ## Pipeline ### Step 1 — Compile @@ -14,7 +24,7 @@ Expected: no output (clean compile). Report any errors. ```bash mvn checkstyle:check -q 2>&1 | tail -20 ``` -Expected: BUILD SUCCESS. Report any violations (excludes `generated/` per pom.xml config). +Note: `failsOnError=false` in pom.xml means the build will not fail even if violations exist — check the output for `[WARN]` checkstyle lines. Violations are excluded from `generated/` by pom config. ### Step 3 — Build ```bash diff --git a/.claude/settings.json b/.claude/settings.json index be1f09bc..3c084d46 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n subprocess.run(['mvn', 'checkstyle:check', '-q'], capture_output=True)\n\"" + "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n r = subprocess.run(['mvn', 'checkstyle:check', '-q'], capture_output=True, text=True)\n out = (r.stdout + r.stderr).strip()\n if out:\n lines = out.splitlines()\n print('\\n'.join(lines[-20:]))\n\"" } ] }, diff --git a/CLAUDE.md b/CLAUDE.md index 1c7fa068..30753abc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,11 +21,13 @@ src/ main/java/com/skyflow/ config/ # VaultConfig, Credentials, ConnectionConfig vault/ - controller/ # VaultController — core SDK logic, API call orchestration + controller/ # VaultController, AuditController, BinLookupController, + # ConnectionController, DetectController data/ # Request/Response objects: InsertRequest, GetResponse, etc. tokens/ # DetokenizeRequest/Response, TokenizeRequest/Response connection/ # InvokeConnectionRequest/Response audit/ # ListEventRequest/Response + bin/ # GetBinRequest/Response (BIN lookup) detect/ # Deidentify/Reidentify requests/responses serviceaccount/ util/ # BearerToken, SignedDataTokens — credential parsing + JWT @@ -35,7 +37,7 @@ src/ generated/ # ← FERN-GENERATED, DO NOT EDIT test/java/com/skyflow/ ... # JUnit 4 tests mirroring the main structure -samples/ # Standalone Maven project with usage examples +samples/ # Standalone Maven project — com.example.vault / .serviceaccount / .detect / .connection docs/ superpowers/ specs/ # Design specs for in-progress features @@ -79,6 +81,19 @@ The SDK reads a `credentials.json` file for service account authentication. The The legacy all-caps forms (`clientID`, `keyID`, `tokenURI`) are accepted as fallbacks for migration. +## Known Pre-existing Test Failures + +These failures exist on `main` and are **not regressions** — do not investigate them unless specifically asked: + +| Test class | Failure | Cause | +|---|---|---| +| `HttpUtilityTests` | `InaccessibleObject` (all tests) | JDK 21 + PowerMock incompatibility — PowerMock cannot reflect into `java.net` | +| `TokenTests#testExpiredTokenForIsExpiredToken` | Environment error | Requires live credentials | +| `VaultClientTests#testSetBearerTokenWithEnvCredentials` | Environment error | Requires `SKYFLOW_CREDENTIALS` env var | +| `ConnectionClientTests#testSetBearerTokenWithEnvCredentials` | Environment error | Requires `SKYFLOW_CREDENTIALS` env var | + +Run `mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR"` to see current baseline (374 tests, ~5 failures, ~4 errors). + ## Active Work See `docs/superpowers/specs/` for in-progress design specs and `docs/superpowers/plans/` for implementation plans. From 93a1150b62d01bf6aef73c7e97105715f4311142 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 11:43:24 +0530 Subject: [PATCH 14/48] docs: add v2 backward compat + deprecation warnings implementation plan 6-task TDD plan: restore skyflow_id key alongside skyflowId in Get/Query responses, add WARN deprecation logs for old credential fields, Javadoc on affected response methods. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...4-v2-backward-compatibility-deprecation.md | 486 ++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md new file mode 100644 index 00000000..ff9752f8 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -0,0 +1,486 @@ +# V2 Backward Compatibility — Deprecation Warnings Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore backward compatibility for v2 customers by keeping old public interface forms alongside new ones, and emit `@Deprecated`-style WARN log messages when the old forms are used. + +**Architecture:** Two independent changes. (1) The `skyflow_id` response key was removed — it must be restored alongside `skyflowId` so both exist in the map simultaneously; a WARN log is emitted per response build when the old key is present. (2) The credential field fallback (`clientID`/`keyID`/`tokenURI`) already works silently — add WARN logs when the old form triggers the fallback path. All deprecation messages use `LogUtil.printWarningLog()` which already exists and respects the caller's configured `LogLevel`. + +**Tech Stack:** Java 11+, JUnit 4, Maven (`mvn test`) + +**Design context:** `docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md` + +--- + +## Breaking-Change Summary + +| Change | Status | Fix needed | +|---|---|---| +| `skyflow_id` removed from Get/Query response maps | **BREAKING** | Keep both `skyflow_id` + `skyflowId`; emit WARN | +| `clientID`/`keyID`/`tokenURI` replaced in BearerToken | Not breaking (fallback exists) | Add WARN log on old-form fallback | +| `clientID`/`keyID` replaced in SignedDataTokens | Not breaking (fallback exists) | Add WARN log on old-form fallback | +| `getErrors()` added to QueryResponse | Not breaking (additive) | No change needed | + +--- + +## File Map + +| File | Change | +|---|---| +| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 4 deprecation warning log entries | +| `src/main/java/com/skyflow/vault/controller/VaultController.java` | Keep `skyflow_id` key alongside `skyflowId`; emit WARN per record | +| `src/main/java/com/skyflow/vault/data/GetResponse.java` | Add `@deprecated` Javadoc on `getData()` for `skyflow_id` key | +| `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `@deprecated` Javadoc on `getFields()` for `skyflow_id` key | +| `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java` | Add WARN log when `clientID`/`keyID`/`tokenURI` fallback fires | +| `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java` | Add WARN log when `clientID`/`keyID` fallback fires | +| `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` | Update existing tests: assert BOTH `skyflow_id` and `skyflowId` present | +| `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` | Existing tests unchanged (old-form already passes); add comment | +| `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` | Existing tests unchanged | + +--- + +## Task 1: Add deprecation log entries to InfoLogs + +**Files:** +- Modify: `src/main/java/com/skyflow/logs/InfoLogs.java` + +### Background +`InfoLogs` is an enum that holds all INFO-level log message strings. Deprecation warnings use `LogUtil.printWarningLog(String)` which takes a plain string — but following the codebase convention of centralising messages in enums, we add the deprecation messages here. They will be passed as `InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()` etc. + +- [ ] **Step 1: Add deprecation entries to InfoLogs enum** + +Open `src/main/java/com/skyflow/logs/InfoLogs.java`. Find the last enum entry before the blank line and constructor (around line 98). Add these four entries in a new section: + +```java + // Deprecation warnings — v2 backward compat, to be removed in v3 + DEPRECATED_SKYFLOW_ID_KEY("Response map key 'skyflow_id' is deprecated in v2 and will be removed in v3. Use 'skyflowId' instead."), + DEPRECATED_CREDENTIAL_CLIENT_ID("Credential field 'clientID' is deprecated in v2. Use 'clientId' instead."), + DEPRECATED_CREDENTIAL_KEY_ID("Credential field 'keyID' is deprecated in v2. Use 'keyId' instead."), + DEPRECATED_CREDENTIAL_TOKEN_URI("Credential field 'tokenURI' is deprecated in v2. Use 'tokenUri' instead."); +``` + +Note: The last existing entry before your addition ends with a comma. Your last entry (`DEPRECATED_CREDENTIAL_TOKEN_URI`) ends with a semicolon `;` to close the enum constant list — verify you are replacing the existing semicolon, not duplicating it. + +- [ ] **Step 2: Verify compilation** + +```bash +mvn compile -q 2>&1 | tail -10 +``` + +Expected: no output (clean compile). + +- [ ] **Step 3: Commit** + +```bash +git add src/main/java/com/skyflow/logs/InfoLogs.java +git commit -m "chore: add deprecation warning log entries to InfoLogs" +``` + +--- + +## Task 2: Restore `skyflow_id` key in Get/Query response maps + +**Files:** +- Modify: `src/main/java/com/skyflow/vault/controller/VaultController.java:121-165` +- Modify: `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` + +### Background +`getFormattedGetRecord` and `getFormattedQueryRecord` currently do: +```java +if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.remove("skyflow_id")); // BREAKS customers using skyflow_id +} +``` +The `remove` deletes `skyflow_id` from the map. We must change this to a **copy** (not a move): put `skyflowId` AND keep `skyflow_id`. Emit one WARN log per record that contains the old key. + +The existing tests `testGetFormattedGetRecordNormalisesSkyflowId` and `testGetFormattedQueryRecordNormalisesSkyflowId` assert `skyflow_id` is **absent** — those assertions must be flipped. + +- [ ] **Step 1: Update the existing tests to assert BOTH keys present** + +In `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java`, find `testGetFormattedGetRecordNormalisesSkyflowId` and `testGetFormattedQueryRecordNormalisesSkyflowId` and update their assertions: + +```java +@Test +public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "abc-123"); + fields.put("name", "John"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + // Both keys must be present — skyflow_id kept for v2 backward compat, skyflowId is the new form + Assert.assertEquals("skyflowId should be present (new form)", "abc-123", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "abc-123", result.get("skyflow_id")); + Assert.assertEquals("other fields should be preserved", "John", result.get("name")); +} + +@Test +public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { + Map fields = new HashMap<>(); + fields.put("skyflow_id", "xyz-456"); + fields.put("email", "test@example.com"); + V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + // Both keys must be present — skyflow_id kept for v2 backward compat, skyflowId is the new form + Assert.assertEquals("skyflowId should be present (new form)", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "xyz-456", result.get("skyflow_id")); + Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); +} + +@Test +public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception { + Map tokens = new HashMap<>(); + tokens.put("skyflow_id", "tok-789"); + tokens.put("card_number", "tok-card-abc"); + V1FieldRecords record = V1FieldRecords.builder().tokens(tokens).build(); + + Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); + method.setAccessible(true); + @SuppressWarnings("unchecked") + HashMap result = (HashMap) method.invoke(null, record); + + // Both keys must be present in tokens branch too + Assert.assertEquals("skyflowId should be present (new form)", "tok-789", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "tok-789", result.get("skyflow_id")); + Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); +} +``` + +- [ ] **Step 2: Run the tests to confirm they fail (skyflow_id is still being removed)** + +```bash +mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId+testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch -q 2>&1 | tail -20 +``` + +Expected: FAIL — assertions on `skyflow_id` being present fail because it is currently removed. + +- [ ] **Step 3: Update `getFormattedGetRecord` in VaultController.java** + +Change the rename block from a **move** to a **copy** and add a WARN log. The import `com.skyflow.logs.InfoLogs` is already present. + +Replace: +```java + if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.remove("skyflow_id")); + } +``` + +With: +```java + if (getRecord.containsKey("skyflow_id")) { + getRecord.put("skyflowId", getRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); + } +``` + +- [ ] **Step 4: Update `getFormattedQueryRecord` in VaultController.java** + +Replace: +```java + if (queryRecord.containsKey("skyflow_id")) { + queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); + } +``` + +With: +```java + if (queryRecord.containsKey("skyflow_id")) { + queryRecord.put("skyflowId", queryRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); + } +``` + +- [ ] **Step 5: Run the tests to confirm they pass** + +```bash +mvn test -pl . -Dtest=VaultControllerTests -q 2>&1 | tail -20 +``` + +Expected: all 11 tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/main/java/com/skyflow/vault/controller/VaultController.java \ + src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +git commit -m "fix: restore skyflow_id key in Get/Query responses for v2 backward compat + +Both skyflow_id (deprecated, v2) and skyflowId (new form) are now present +in response maps simultaneously. WARN log emitted per record to signal +migration path to callers." +``` + +--- + +## Task 3: Add deprecation Javadoc to GetResponse and QueryResponse + +**Files:** +- Modify: `src/main/java/com/skyflow/vault/data/GetResponse.java` +- Modify: `src/main/java/com/skyflow/vault/data/QueryResponse.java` + +### Background +Customers reading `getData()` or `getFields()` should see a compiler-visible signal that `skyflow_id` is a deprecated key in the returned map, with guidance to migrate to `skyflowId`. + +- [ ] **Step 1: Add Javadoc to `GetResponse.getData()`** + +In `src/main/java/com/skyflow/vault/data/GetResponse.java`, add Javadoc above `getData()`: + +```java + /** + * Returns the list of record maps from the Get 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 as of v2 and will be removed in v3. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility.

+ */ + public ArrayList> getData() { + return data; + } +``` + +- [ ] **Step 2: Add Javadoc to `QueryResponse.getFields()`** + +In `src/main/java/com/skyflow/vault/data/QueryResponse.java`, add Javadoc above `getFields()`: + +```java + /** + * 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 as of v2 and will be removed in v3. Use {@code skyflowId} instead. + * Both keys are present simultaneously in v2 for backward compatibility.

+ */ + public ArrayList> getFields() { + return fields; + } +``` + +- [ ] **Step 3: Verify compilation** + +```bash +mvn compile -q 2>&1 | tail -10 +``` + +Expected: no output. + +- [ ] **Step 4: Commit** + +```bash +git add src/main/java/com/skyflow/vault/data/GetResponse.java \ + src/main/java/com/skyflow/vault/data/QueryResponse.java +git commit -m "docs: add deprecation Javadoc for skyflow_id key in GetResponse and QueryResponse" +``` + +--- + +## Task 4: Add deprecation WARN logs in BearerToken for old credential field names + +**Files:** +- Modify: `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java:102-127` + +### Background +`getBearerTokenFromCredentials` already has fallback: tries `clientId` first, then `clientID`. When the fallback triggers (new form returns null, old form returns non-null), we emit a WARN log so users know to migrate their credentials file. + +- [ ] **Step 1: Add WARN logs to the three fallback paths in `BearerToken.java`** + +Current code (lines 102–127): +```java + // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { ... throw ... } + + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { ... throw ... } + + JsonElement tokenUri = credentials.get("tokenUri"); + if (tokenUri == null) { + tokenUri = credentials.get("tokenURI"); + } + if (tokenUri == null) { ... throw ... } +``` + +Replace with — adding a WARN log inside each fallback `if` block: + +```java + // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + if (clientId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); + } + } + if (clientId == null) { + LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); + } + + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + if (keyId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); + } + } + if (keyId == null) { + LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); + } + + JsonElement tokenUri = credentials.get("tokenUri"); + if (tokenUri == null) { + tokenUri = credentials.get("tokenURI"); + if (tokenUri != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_TOKEN_URI.getLog()); + } + } + if (tokenUri == null) { + LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); + } +``` + +- [ ] **Step 2: Verify compilation and run BearerToken tests** + +```bash +mvn compile -q 2>&1 | tail -5 +mvn test -pl . -Dtest=BearerTokenTests -q 2>&1 | tail -10 +``` + +Expected: clean compile, 17/17 tests pass. The existing test `testBearerTokenWithOldFormCredentialKeys` (which uses `clientID`/`keyID`/`tokenURI`) continues to pass — now with a WARN log emitted at runtime. + +- [ ] **Step 3: Commit** + +```bash +git add src/main/java/com/skyflow/serviceaccount/util/BearerToken.java +git commit -m "feat: emit deprecation WARN log when legacy clientID/keyID/tokenURI credential fields are used in BearerToken" +``` + +--- + +## Task 5: Add deprecation WARN logs in SignedDataTokens for old credential field names + +**Files:** +- Modify: `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java:103-122` + +### Background +Same as Task 4 but for `SignedDataTokens`. Only `clientId`/`keyId` — no `tokenUri` in this class. + +- [ ] **Step 1: Add WARN logs to the two fallback paths in `SignedDataTokens.java`** + +Current code (lines 103–122): +```java + // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + } + if (clientId == null) { ... throw ... } + + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + } + if (keyId == null) { ... throw ... } +``` + +Replace with: + +```java + // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration + JsonElement clientId = credentials.get("clientId"); + if (clientId == null) { + clientId = credentials.get("clientID"); + if (clientId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); + } + } + if (clientId == null) { + LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); + } + + JsonElement keyId = credentials.get("keyId"); + if (keyId == null) { + keyId = credentials.get("keyID"); + if (keyId != null) { + LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); + } + } + if (keyId == null) { + LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); + } +``` + +Also verify that `InfoLogs` is already imported in `SignedDataTokens.java`. If not, add: +```java +import com.skyflow.logs.InfoLogs; +``` + +- [ ] **Step 2: Verify compilation and run SignedDataTokens tests** + +```bash +mvn compile -q 2>&1 | tail -5 +mvn test -pl . -Dtest=SignedDataTokensTests -q 2>&1 | tail -10 +``` + +Expected: clean compile, 15/15 tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java +git commit -m "feat: emit deprecation WARN log when legacy clientID/keyID credential fields are used in SignedDataTokens" +``` + +--- + +## Task 6: Final verification — full test suite + +- [ ] **Step 1: Run full test suite** + +```bash +mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR" | tail -10 +``` + +Expected baseline: 374 tests, ~5 failures, ~4 errors (all pre-existing — see `CLAUDE.md` for the list). No new failures. + +- [ ] **Step 2: Verify both keys appear in a sample response** + +Manually verify by running a grep to confirm the implementation is correct: + +```bash +grep -n "skyflow_id\|skyflowId\|DEPRECATED" src/main/java/com/skyflow/vault/controller/VaultController.java +``` + +Expected output includes lines like: +``` +getRecord.put("skyflowId", getRecord.get("skyflow_id")); +LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); +``` + +and no `getRecord.remove("skyflow_id")`. + +- [ ] **Step 3: Commit (if any final cleanup needed)** + +```bash +git commit --allow-empty -m "chore: v2 backward compat + deprecation warnings complete" +``` From fd07ab672acfb3b3d22cc886ef6f5ecbada83483 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 11:46:26 +0530 Subject: [PATCH 15/48] docs: update deprecation messages to say 'upcoming release' not 'v3' Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...26-05-14-v2-backward-compatibility-deprecation.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md index ff9752f8..f749e8ff 100644 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -53,10 +53,10 @@ Open `src/main/java/com/skyflow/logs/InfoLogs.java`. Find the last enum entry be ```java // Deprecation warnings — v2 backward compat, to be removed in v3 - DEPRECATED_SKYFLOW_ID_KEY("Response map key 'skyflow_id' is deprecated in v2 and will be removed in v3. Use 'skyflowId' instead."), - DEPRECATED_CREDENTIAL_CLIENT_ID("Credential field 'clientID' is deprecated in v2. Use 'clientId' instead."), - DEPRECATED_CREDENTIAL_KEY_ID("Credential field 'keyID' is deprecated in v2. Use 'keyId' instead."), - DEPRECATED_CREDENTIAL_TOKEN_URI("Credential field 'tokenURI' is deprecated in v2. Use 'tokenUri' instead."); + DEPRECATED_SKYFLOW_ID_KEY("Response map key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), + DEPRECATED_CREDENTIAL_CLIENT_ID("Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), + DEPRECATED_CREDENTIAL_KEY_ID("Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), + DEPRECATED_CREDENTIAL_TOKEN_URI("Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."); ``` Note: The last existing entry before your addition ends with a comma. Your last entry (`DEPRECATED_CREDENTIAL_TOKEN_URI`) ends with a semicolon `;` to close the enum constant list — verify you are replacing the existing semicolon, not duplicating it. @@ -240,7 +240,7 @@ In `src/main/java/com/skyflow/vault/data/GetResponse.java`, add Javadoc above `g * field name/value pairs for the record. * *

Deprecation notice: The {@code skyflow_id} key in each record map is - * deprecated as of v2 and will be removed in v3. Use {@code skyflowId} instead. + * 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() { @@ -258,7 +258,7 @@ In `src/main/java/com/skyflow/vault/data/QueryResponse.java`, add Javadoc above * field name/value pairs for the record. * *

Deprecation notice: The {@code skyflow_id} key in each record map is - * deprecated as of v2 and will be removed in v3. Use {@code skyflowId} instead. + * 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() { From 6c1de31b6c1541aae70c5f4f818e2e35e7b80785 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 11:50:29 +0530 Subject: [PATCH 16/48] docs: add [DEPRECATED] prefix to deprecation log messages per industry standard Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../2026-05-14-v2-backward-compatibility-deprecation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md index f749e8ff..8ddcd29d 100644 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -53,10 +53,10 @@ Open `src/main/java/com/skyflow/logs/InfoLogs.java`. Find the last enum entry be ```java // Deprecation warnings — v2 backward compat, to be removed in v3 - DEPRECATED_SKYFLOW_ID_KEY("Response map key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), - DEPRECATED_CREDENTIAL_CLIENT_ID("Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead."), - DEPRECATED_CREDENTIAL_KEY_ID("Credential field 'keyID' is deprecated and will be removed in an upcoming release. Use 'keyId' instead."), - DEPRECATED_CREDENTIAL_TOKEN_URI("Credential field 'tokenURI' is deprecated and will be removed in an upcoming release. Use 'tokenUri' instead."); + DEPRECATED_SKYFLOW_ID_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' 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."); ``` Note: The last existing entry before your addition ends with a comma. Your last entry (`DEPRECATED_CREDENTIAL_TOKEN_URI`) ends with a semicolon `;` to close the enum constant list — verify you are replacing the existing semicolon, not duplicating it. From de6e4b44605299dced9012d8dcb0bca8f4f4d9f8 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 11:54:34 +0530 Subject: [PATCH 17/48] docs: add PM-facing document for v2 public interface changes and deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-technical overview of credential field renames and skyflow_id response key deprecation — covers customer impact, deprecation warnings, migration guide, and what is NOT changing. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- docs/v2-public-interface-changes.md | 129 ++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/v2-public-interface-changes.md diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md new file mode 100644 index 00000000..a3036947 --- /dev/null +++ b/docs/v2-public-interface-changes.md @@ -0,0 +1,129 @@ +# Skyflow Java SDK — Public Interface Changes & Deprecation Notice + +**Audience:** Product Managers, Technical Program Managers, Customer Success +**SDK:** skyflow-java +**Affected versions:** v2.x (current) → upcoming release + +--- + +## Overview + +As part of aligning the Skyflow Java SDK with cross-language naming standards, a set of public-facing field names and response keys are being updated. All changes are designed to be **non-breaking for existing customers** — old forms continue to work alongside new ones, with deprecation warnings logged at runtime to guide migration. + +A future release will remove the deprecated forms entirely. No removal date is set yet. + +--- + +## What Is Changing + +### 1. Credentials file field names + +When customers authenticate using a service account credentials JSON file, the field names inside that file are changing to follow Java naming conventions (lowercase acronyms). + +| Old field name (deprecated) | New field name | Used in | +|---|---|---| +| `clientID` | `clientId` | `credentials.json` | +| `keyID` | `keyId` | `credentials.json` | +| `tokenURI` | `tokenUri` | `credentials.json` | + +**Customer impact:** Customers with existing credentials files using the old field names (`clientID`, `keyID`, `tokenURI`) will continue to work without any changes. A deprecation warning will appear in their application logs recommending they update to the new field names. + +**Example of the warning customers will see in their logs:** +``` +[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead. +``` + +--- + +### 2. Response field key in Get and Query operations + +When customers retrieve records from a vault using the **Get** or **Query** operations, each record includes a `skyflow_id` field identifying the record. This key name is changing to follow Java camelCase conventions. + +| Old key (deprecated) | New key | Affected operations | +|---|---|---| +| `skyflow_id` | `skyflowId` | Get, Query | + +**Customer impact:** Both `skyflow_id` and `skyflowId` will be present in response records simultaneously. Customers accessing `skyflow_id` today continue to receive the correct value. A deprecation warning will be logged once per record to prompt migration. + +**Example of the warning customers will see in their logs:** +``` +[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead. +``` + +> **Note:** Insert and Update operations already return `skyflowId` (camelCase) and are unaffected. + +--- + +## What Is NOT Changing + +- The Java method names customers call (e.g. `.insert()`, `.get()`, `.query()`) +- The request builder APIs (e.g. `InsertRequest.builder()`) +- Any vault configuration APIs (`VaultConfig`, `Credentials` setters) +- Authentication behaviour — credentials files still work identically +- Any connection, detect, audit, or tokenize interfaces + +--- + +## Deprecation Strategy + +| Phase | What happens | Timeline | +|---|---|---| +| **Now (v2.x)** | Old forms still work. Deprecation `[DEPRECATED]` warning logged at WARN level when old form is used. New forms also accepted. | Current | +| **Upcoming release** | Old forms removed. Only new forms accepted. Customers who have not migrated will see errors. | TBD | + +Customers can suppress deprecation warnings by updating to the new field names at any time — no other code changes are required. + +--- + +## Customer Migration Guide + +### Credentials file + +Update `credentials.json`: + +```json +// Before (deprecated) +{ + "clientID": "...", + "keyID": "...", + "tokenURI": "...", + "privateKey": "..." +} + +// After (new — no other changes needed) +{ + "clientId": "...", + "keyId": "...", + "tokenUri": "...", + "privateKey": "..." +} +``` + +### Get / Query response access + +```java +// Before (deprecated — still works in v2, removed in upcoming release) +String id = record.get("skyflow_id").toString(); + +// After (new form — works in current and all future versions) +String id = record.get("skyflowId").toString(); +``` + +--- + +## How to Check If Your Integration Is Affected + +Set log level to `WARN` or higher. If you see any `[DEPRECATED]` entries in your application logs after upgrading, your integration is using an old form and should be updated before the next major release. + +```java +Skyflow client = Skyflow.builder() + .setLogLevel(LogLevel.WARN) + ... + .build(); +``` + +--- + +## Questions + +For technical questions, contact the SDK team. For release timeline questions, contact your Skyflow account representative. From 541da959463c11463368d1db4bb07e321266c331 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 12:13:47 +0530 Subject: [PATCH 18/48] =?UTF-8?q?docs:=20add=20downloadURL=E2=86=92downloa?= =?UTF-8?q?dUrl=20deprecation=20to=20plan=20and=20PM=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deprecation plan: add Task 6 for GetRequest + DetokenizeRequest with @Deprecated annotation approach (compile-time signal vs runtime log) - PM doc: add section 3 for downloadURL rename with migration example Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...4-v2-backward-compatibility-deprecation.md | 196 +++++++++++++++++- docs/v2-public-interface-changes.md | 36 ++++ 2 files changed, 228 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md index 8ddcd29d..58fd1506 100644 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -2,9 +2,9 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Restore backward compatibility for v2 customers by keeping old public interface forms alongside new ones, and emit `@Deprecated`-style WARN log messages when the old forms are used. +**Goal:** Restore backward compatibility for v2 customers by keeping old public interface forms alongside new ones, and emit deprecation signals when the old forms are used. -**Architecture:** Two independent changes. (1) The `skyflow_id` response key was removed — it must be restored alongside `skyflowId` so both exist in the map simultaneously; a WARN log is emitted per response build when the old key is present. (2) The credential field fallback (`clientID`/`keyID`/`tokenURI`) already works silently — add WARN logs when the old form triggers the fallback path. All deprecation messages use `LogUtil.printWarningLog()` which already exists and respects the caller's configured `LogLevel`. +**Architecture:** Three independent changes. (1) The `skyflow_id` response key was removed — it must be restored alongside `skyflowId` so both exist in the map simultaneously; a WARN log is emitted per response build when the old key is present. (2) The credential field fallback (`clientID`/`keyID`/`tokenURI`) already works silently — add WARN logs when the old form triggers the fallback path. (3) `downloadURL` method names on `GetRequest` and `DetokenizeRequest` violate the acronym-as-word rule — keep the old `@Deprecated` methods alongside new `downloadUrl` methods. All runtime deprecation messages use `LogUtil.printWarningLog()`; method-level deprecation uses the standard Java `@Deprecated` annotation. **Tech Stack:** Java 11+, JUnit 4, Maven (`mvn test`) @@ -20,6 +20,7 @@ | `clientID`/`keyID`/`tokenURI` replaced in BearerToken | Not breaking (fallback exists) | Add WARN log on old-form fallback | | `clientID`/`keyID` replaced in SignedDataTokens | Not breaking (fallback exists) | Add WARN log on old-form fallback | | `getErrors()` added to QueryResponse | Not breaking (additive) | No change needed | +| `downloadURL` → `downloadUrl` in GetRequest & DetokenizeRequest | **BREAKING** | Keep `@Deprecated` old methods; add new `downloadUrl` methods | --- @@ -27,7 +28,9 @@ | File | Change | |---|---| -| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 4 deprecation warning log entries | +| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 5 deprecation warning log entries (4 existing + 1 for downloadURL) | +| `src/main/java/com/skyflow/vault/data/GetRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` | +| `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` | | `src/main/java/com/skyflow/vault/controller/VaultController.java` | Keep `skyflow_id` key alongside `skyflowId`; emit WARN per record | | `src/main/java/com/skyflow/vault/data/GetResponse.java` | Add `@deprecated` Javadoc on `getData()` for `skyflow_id` key | | `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `@deprecated` Javadoc on `getFields()` for `skyflow_id` key | @@ -453,7 +456,192 @@ git commit -m "feat: emit deprecation WARN log when legacy clientID/keyID creden --- -## Task 6: Final verification — full test suite +## Task 6: Deprecate `downloadURL` → `downloadUrl` in GetRequest and DetokenizeRequest + +**Files:** +- Modify: `src/main/java/com/skyflow/vault/data/GetRequest.java` +- Modify: `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` +- Modify: `src/main/java/com/skyflow/logs/InfoLogs.java` (add one entry) + +### Background +`getDownloadURL()` and builder `.downloadURL()` use all-caps `URL`, violating the same acronym-as-word rule as `clientID`/`tokenURI`. Since these are Java method names (not map keys), we use the standard `@Deprecated` annotation + Javadoc, which gives callers a **compile-time warning** in their IDE. No runtime `LogUtil` log is needed — the annotation is the industry standard signal for method deprecation. Keep the old methods as delegates to the new ones so existing code compiles without changes. + +- [ ] **Step 1: Add `DEPRECATED_DOWNLOAD_URL` to InfoLogs.java** + +Open `src/main/java/com/skyflow/logs/InfoLogs.java` and add one entry to the deprecation section: + +```java + DEPRECATED_DOWNLOAD_URL("[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead."), +``` + +- [ ] **Step 2: Write failing tests for new `downloadUrl` methods** + +Add these tests to `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` (or a new `GetRequestTest.java`): + +```java +import com.skyflow.vault.data.GetRequest; +import com.skyflow.vault.tokens.DetokenizeRequest; + +@Test +public void testGetRequestDownloadUrlNewForm() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadUrl(true) + .build(); + Assert.assertTrue("downloadUrl(true) should be set", request.getDownloadUrl()); +} + +@Test +public void testGetRequestDownloadURLOldFormStillWorks() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadURL(true) + .build(); + // Old method delegates to new — both accessors return the same value + Assert.assertTrue("old downloadURL() should still work", request.getDownloadURL()); + Assert.assertTrue("new getDownloadUrl() should also return same value", request.getDownloadUrl()); +} + +@Test +public void testDetokenizeRequestDownloadUrlNewForm() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadUrl(true) + .build(); + Assert.assertTrue("downloadUrl(true) should be set", request.getDownloadUrl()); +} +``` + +- [ ] **Step 3: Run tests to confirm they fail** + +```bash +mvn test -pl . -Dtest=VaultControllerTests#testGetRequestDownloadUrlNewForm+testGetRequestDownloadURLOldFormStillWorks+testDetokenizeRequestDownloadUrlNewForm -q 2>&1 | tail -10 +``` + +Expected: compile error — `downloadUrl()` and `getDownloadUrl()` methods do not exist yet. + +- [ ] **Step 4: Update `GetRequest.java`** + +In `src/main/java/com/skyflow/vault/data/GetRequest.java`: + +**On the request class** — add new getter, mark old one `@Deprecated`: +```java + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated + public Boolean getDownloadURL() { + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; + } +``` + +**On the builder** — rename the field and add both builder methods: +```java + public static final class GetRequestBuilder { + // ... other fields ... + private Boolean downloadUrl; // renamed from downloadURL + + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated + public GetRequestBuilder downloadURL(Boolean downloadURL) { + return downloadUrl(downloadURL); + } + + public GetRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl; + return this; + } + } +``` + +Also update the `getDownloadURL()` accessor in the request body (the non-builder getter) to delegate: +```java + public Boolean getDownloadURL() { + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; + } +``` + +Note: the existing `getDownloadURL()` in the request class (not the builder) currently reads `this.builder.downloadURL`. After renaming the field to `downloadUrl`, update the reference accordingly. + +- [ ] **Step 5: Update `DetokenizeRequest.java`** + +Apply the identical pattern in `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java`: + +**On the request class:** +```java + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated + public Boolean getDownloadURL() { + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; + } +``` + +**On the builder:** +```java + private Boolean downloadUrl; // renamed from downloadURL + + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated + public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { + return downloadUrl(downloadURL); + } + + public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl; + return this; + } +``` + +- [ ] **Step 6: Run the new tests to confirm they pass** + +```bash +mvn test -pl . -Dtest=VaultControllerTests#testGetRequestDownloadUrlNewForm+testGetRequestDownloadURLOldFormStillWorks+testDetokenizeRequestDownloadUrlNewForm -q 2>&1 | tail -10 +``` + +Expected: all 3 pass. + +- [ ] **Step 7: Run full suite to confirm no regressions** + +```bash +mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR" | tail -5 +``` + +Expected: baseline only (no new failures). + +- [ ] **Step 8: Commit** + +```bash +git add src/main/java/com/skyflow/vault/data/GetRequest.java \ + src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java \ + src/main/java/com/skyflow/logs/InfoLogs.java \ + src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +git commit -m "feat: deprecate downloadURL in favour of downloadUrl in GetRequest and DetokenizeRequest + +Old downloadURL() methods kept as @Deprecated delegates for v2 backward +compat. New downloadUrl() methods follow the acronym-as-word convention +consistent with skyflowId, clientId, tokenUri." +``` + +--- + +## Task 8: Final verification — full test suite - [ ] **Step 1: Run full test suite** diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md index a3036947..013b2ff9 100644 --- a/docs/v2-public-interface-changes.md +++ b/docs/v2-public-interface-changes.md @@ -54,6 +54,24 @@ When customers retrieve records from a vault using the **Get** or **Query** oper --- +### 3. `downloadURL` method names in Get and Detokenize operations + +Two method names used when configuring Get and Detokenize requests are changing to follow the same naming convention as the other fields above (`URL` → `Url`). + +| Old method (deprecated) | New method | Used in | +|---|---|---| +| `.downloadURL(true)` builder method | `.downloadUrl(true)` | `GetRequest.builder()`, `DetokenizeRequest.builder()` | +| `.getDownloadURL()` | `.getDownloadUrl()` | `GetRequest`, `DetokenizeRequest` | + +**Customer impact:** Existing code using `.downloadURL()` or `.getDownloadURL()` continues to compile and work. IDEs that support Java `@Deprecated` annotation will show a visual strikethrough on the old method name as a migration hint. No runtime behavior changes. + +**Example of the IDE/compiler warning customers will see:** +``` +[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead. +``` + +--- + ## What Is NOT Changing - The Java method names customers call (e.g. `.insert()`, `.get()`, `.query()`) @@ -77,6 +95,24 @@ Customers can suppress deprecation warnings by updating to the new field names a ## Customer Migration Guide +### Get / Detokenize `downloadURL` → `downloadUrl` + +```java +// Before (deprecated — still compiles in v2, removed in upcoming release) +GetRequest request = GetRequest.builder() + .table("persons") + .ids(ids) + .downloadURL(true) // ← deprecated + .build(); + +// After +GetRequest request = GetRequest.builder() + .table("persons") + .ids(ids) + .downloadUrl(true) // ← new form + .build(); +``` + ### Credentials file Update `credentials.json`: From bf8f83e05715a8cfd92aa7824e645dec72f2db81 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 12:50:19 +0530 Subject: [PATCH 19/48] fix: remove SDK-level field value null/empty validation from Insert and Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Skyflow API accepts additionalProperties of Any type including null and empty strings. SDK should not add validation on top of BE — pass through and let BE decide. Removed: - value == null/isEmpty check in validateInsertRequest - value == null/isEmpty check in validateUpdateRequest - value == null/isEmpty check in validateTokensMapWithTokenStrict - values.isEmpty() check in validateInsertRequest (no minItems in API spec) Kept: - values == null check (NPE guard — cannot iterate null array) - key == null/isEmpty check (null keys cannot be JSON-serialized) Deleted 6 tests that asserted on the removed behaviour. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- docs/v2-public-interface-changes.md | 20 +++++++ .../utils/validations/Validations.java | 32 ----------- .../com/skyflow/vault/data/InsertTests.java | 49 ---------------- .../com/skyflow/vault/data/UpdateTests.java | 57 ------------------- 4 files changed, 20 insertions(+), 138 deletions(-) diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md index 013b2ff9..35c09eaa 100644 --- a/docs/v2-public-interface-changes.md +++ b/docs/v2-public-interface-changes.md @@ -72,6 +72,26 @@ Two method names used when configuring Get and Detokenize requests are changing --- +--- + +## Behaviour Change: Insert and Update field value validation removed + +**Affected operations:** Insert, Update + +Previously the Java SDK threw an error if a record field value was `null` or an empty string `""`. This validation was inconsistent with both the Skyflow API spec (which accepts `additionalProperties: Any type`) and with other SDKs (Node has no such validation). + +**The validation has been removed.** Field values of `null`, `""`, or any type are now passed through to the backend unchanged. The backend is the authoritative source for field-level validation. + +| Scenario | Before | After | +|---|---|---| +| `{"name": null}` | SDK throws `SkyflowException` | Passed to BE ✓ | +| `{"name": ""}` | SDK throws `SkyflowException` | Passed to BE ✓ | +| `records: []` (empty array) | SDK throws `SkyflowException` | Passed to BE ✓ | + +**This is a non-breaking change** — code that was previously failing will now succeed. + +--- + ## What Is NOT Changing - The Java method names customers call (e.g. `.insert()`, `.get()`, `.query()`) diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java index e1f18795..d05ecfdd 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/src/main/java/com/skyflow/utils/validations/Validations.java @@ -296,11 +296,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky ErrorLogs.VALUES_IS_REQUIRED.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.ValuesKeyError.getMessage()); - } else if (values.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_VALUES.getLog(), InterfaceName.INSERT.getName() - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValues.getMessage()); } else if (upsert != null) { if (upsert.trim().isEmpty()) { LogUtil.printErrorLog(Utils.parameterizedString( @@ -324,15 +319,6 @@ public static void validateInsertRequest(InsertRequest insertRequest) throws Sky ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.INSERT.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage()); - } else { - Object value = valuesMap.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()); - } } } } @@ -574,15 +560,6 @@ public static void validateUpdateRequest(UpdateRequest updateRequest) throws Sky ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.getLog(), InterfaceName.UPDATE.getName() )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyKeyInValues.getMessage()); - } else { - Object value = data.get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_VALUES.getLog(), InterfaceName.UPDATE.getName(), key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), - ErrorMessage.EmptyValueInValues.getMessage()); - } } } @@ -875,15 +852,6 @@ private static void validateTokensMapWithTokenStrict( ErrorLogs.MISMATCH_OF_FIELDS_AND_TOKENS.getLog(), interfaceName )); throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MismatchOfFieldsAndTokens.getMessage()); - } else { - Object value = tokensMap.get(key); - if (value == null || value.toString().trim().isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_OR_NULL_VALUE_IN_TOKENS.getLog(), - interfaceName, key - )); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyValueInTokens.getMessage()); - } } } } diff --git a/src/test/java/com/skyflow/vault/data/InsertTests.java b/src/test/java/com/skyflow/vault/data/InsertTests.java index 00399f00..cc7731be 100644 --- a/src/test/java/com/skyflow/vault/data/InsertTests.java +++ b/src/test/java/com/skyflow/vault/data/InsertTests.java @@ -171,21 +171,6 @@ public void testNoValuesInInsertRequestValidations() { } } - @Test - public void testEmptyValuesInInsertRequestValidations() { - InsertRequest request = InsertRequest.builder().table(table).values(values).build(); - try { - Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - @Test public void testEmptyKeyInValuesInInsertRequestValidations() { valueMap.put("", "test_value_3"); @@ -203,23 +188,6 @@ public void testEmptyKeyInValuesInInsertRequestValidations() { } } - @Test - public void testEmptyValueInValuesInInsertRequestValidations() { - valueMap.put("test_column_3", ""); - values.add(valueMap); - InsertRequest request = InsertRequest.builder().table(table).values(values).build(); - try { - Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - @Test public void testEmptyUpsertInInsertRequestValidations() { values.add(valueMap); @@ -388,23 +356,6 @@ public void testEmptyKeyInTokensInInsertRequestValidations() { } } - @Test - public void testEmptyValueInTokensInInsertRequestValidations() { - tokenMap.put("test_column_2", ""); - values.add(valueMap); - tokens.add(tokenMap); - InsertRequest request = InsertRequest.builder() - .table(table).values(values).tokens(tokens).tokenMode(TokenMode.ENABLE_STRICT) - .build(); - try { - Validations.validateInsertRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); - } - } - @Test public void testInsertResponse() { try { diff --git a/src/test/java/com/skyflow/vault/data/UpdateTests.java b/src/test/java/com/skyflow/vault/data/UpdateTests.java index be702d4e..55e5b811 100644 --- a/src/test/java/com/skyflow/vault/data/UpdateTests.java +++ b/src/test/java/com/skyflow/vault/data/UpdateTests.java @@ -247,44 +247,6 @@ public void testEmptyKeyInValuesInUpdateRequestValidations() { } } - @Test - public void testNullValueInValuesInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - dataMap.put("test_column_3", null); - UpdateRequest request = UpdateRequest.builder().table(table).data(dataMap).build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - - @Test - public void testEmptyValueInValuesInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - dataMap.put("test_column_3", ""); - UpdateRequest request = UpdateRequest.builder().table(table).data(dataMap).build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals( - Utils.parameterizedString(ErrorMessage.EmptyValueInValues.getMessage(), Constants.SDK_PREFIX), - e.getMessage() - ); - } - } - @Test public void testTokensWithTokenModeDisableInUpdateRequestValidations() { dataMap.put("skyflow_id", skyflowID); @@ -419,25 +381,6 @@ public void testNullKeyInTokensInUpdateRequestValidations() { } } - @Test - public void testNullValueInTokensInUpdateRequestValidations() { - dataMap.put("skyflow_id", skyflowID); - dataMap.put("test_column_1", "test_value_1"); - dataMap.put("test_column_2", "test_value_2"); - tokenMap.put("test_column_1", "test_token_1"); - tokenMap.put("test_column_2", null); - UpdateRequest request = UpdateRequest.builder() - .table(table).data(dataMap).tokens(tokenMap).tokenMode(TokenMode.ENABLE_STRICT) - .build(); - try { - Validations.validateUpdateRequest(request); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.EmptyValueInTokens.getMessage(), e.getMessage()); - } - } - @Test public void testUpdateResponse() { try { From 81128760bb6bd6d7af59ab2b66d2a7d25caecc9d Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 14:46:08 +0530 Subject: [PATCH 20/48] docs: add IDE autocomplete behavior for deprecation signals - PM doc: new section explaining how @Deprecated(forRemoval) shows in IntelliJ/VS Code autocomplete (strikethrough, orange underline, tooltip with clickable link to new method) vs runtime WARN log for map keys - Deprecation plan: update downloadURL tasks to use @Deprecated(since="2.1", forRemoval=true) + {link} for stronger IDE signal Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...4-v2-backward-compatibility-deprecation.md | 13 ++++--- docs/v2-public-interface-changes.md | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md index 58fd1506..2056f2a9 100644 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -523,12 +523,15 @@ Expected: compile error — `downloadUrl()` and `getDownloadUrl()` methods do no In `src/main/java/com/skyflow/vault/data/GetRequest.java`: -**On the request class** — add new getter, mark old one `@Deprecated`: +**On the request class** — add new getter, mark old one `@Deprecated(since, forRemoval)`: + +Using `forRemoval = true` triggers an **orange underline** in IntelliJ/VS Code (stronger than plain `@Deprecated` yellow). The `{@link}` in Javadoc creates a clickable link to the new method in the IDE autocomplete tooltip, so the developer sees the replacement inline without leaving autocomplete. + ```java /** * @deprecated Use {@link #getDownloadUrl()} instead. */ - @Deprecated + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { return getDownloadUrl(); } @@ -547,7 +550,7 @@ In `src/main/java/com/skyflow/vault/data/GetRequest.java`: /** * @deprecated Use {@link #downloadUrl(Boolean)} instead. */ - @Deprecated + @Deprecated(since = "2.1", forRemoval = true) public GetRequestBuilder downloadURL(Boolean downloadURL) { return downloadUrl(downloadURL); } @@ -581,7 +584,7 @@ Apply the identical pattern in `src/main/java/com/skyflow/vault/tokens/Detokeniz /** * @deprecated Use {@link #getDownloadUrl()} instead. */ - @Deprecated + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { return getDownloadUrl(); } @@ -598,7 +601,7 @@ Apply the identical pattern in `src/main/java/com/skyflow/vault/tokens/Detokeniz /** * @deprecated Use {@link #downloadUrl(Boolean)} instead. */ - @Deprecated + @Deprecated(since = "2.1", forRemoval = true) public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { return downloadUrl(downloadURL); } diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md index 35c09eaa..d9aa9e34 100644 --- a/docs/v2-public-interface-changes.md +++ b/docs/v2-public-interface-changes.md @@ -14,6 +14,44 @@ A future release will remove the deprecated forms entirely. No removal date is s --- +## How Deprecation Signals Work in Java IDEs + +Customers using modern Java IDEs (IntelliJ IDEA, VS Code, Eclipse) will see the following signals when using deprecated methods or fields. No code changes are required to see these — they appear automatically. + +### Method deprecation (`downloadURL` → `downloadUrl`) + +When a customer types `.downloadU` in their IDE, autocomplete shows both forms simultaneously. The old form is visually marked: + +``` +▼ Autocomplete +────────────────────────────────────────────────── + downloadUrl(Boolean) ← new form, no marker + ~~downloadURL~~(Boolean) ⚠️ ← strikethrough + warning icon +────────────────────────────────────────────────── +``` + +Hovering over the deprecated method shows an inline tooltip: +``` +⚠️ Deprecated. Use downloadUrl(Boolean) instead. +``` + +If a customer selects the deprecated form and uses it in their code, the IDE shows an **orange underline** at the call site — a stronger visual than a plain yellow warning — because the method is marked `forRemoval = true`. + +### Runtime log warnings (credential fields, `skyflow_id` key) + +For changes that cannot use Java annotations (map keys, JSON field names), a `[DEPRECATED]` warning is logged at runtime when the old form is used: + +``` +[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed +in an upcoming release. Use 'skyflowId' instead. +``` + +These appear in the application log at WARN level. Customers running with `LogLevel.WARN` or higher will see them. + +--- + +--- + ## What Is Changing ### 1. Credentials file field names From 57fd8f6bd71beaab77584f360a73d8346d9abfb0 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Thu, 14 May 2026 15:04:00 +0530 Subject: [PATCH 21/48] chore: segregate code smells into dedicated section in code-review command Splits old Section 6 into: - Section 6: Code quality (actionable correctness checks) - Section 7: Code smells (structural signals, flagged at Smell severity) Code smell catalogue covers: long methods/classes, business logic in data classes, toString() with logic, deep nesting, magic numbers, raw HashMap chains, dead code, stale comments, temporary fields. Severity table clarified: Critical/Bug/Edge Case/Quality = fix before merge; Smell = flag and track. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/commands/code-review.md | 113 +++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 22 deletions(-) diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md index 6bd6a69c..23653ac2 100644 --- a/.claude/commands/code-review.md +++ b/.claude/commands/code-review.md @@ -10,42 +10,100 @@ Use `$ARGUMENTS` to determine scope: git diff main...HEAD --name-only | grep '\.java$' | grep -v 'generated' ``` -## What to Review - **Skip entirely:** `src/main/java/com/skyflow/generated/` — Fern-generated REST client, read-only. -### 1. Request / Response / Options patterns +--- + +## 1. Request / Response / Options patterns + - Request builders are plain data holders — validation happens in `Validations.validateXxxRequest()` inside the controller, not in `build()`. Flag if validation logic is duplicated outside `Validations`. - Response getters returning `ArrayList>` is the established SDK pattern — do not flag these as violations. -- All response classes must have `getErrors()` returning `null` (not absent) when no errors. `QueryResponse` is the historical exception — it now has `getErrors()` too. +- All response classes must have `getErrors()` returning `null` (not absent) when no errors. - No separate `*Options` classes exist — options are fields on the request builder itself. +- SDK must not add field-level null/empty validation on top of what the backend enforces. Only structural checks (`table == null`, `values == null`) are permitted. + +--- + +## 2. Error handling -### 2. Error handling - All public methods must declare `throws SkyflowException` - `SkyflowException` must be thrown (not swallowed) on invalid input - No `System.out.println` or bare `e.printStackTrace()` — use `LogUtil` - Catch blocks must not silently drop exceptions +- `catch (Exception e)` without re-throw or explicit handling is a critical issue + +--- + +## 3. Naming conventions -### 3. Naming conventions - Classes: `PascalCase` -- Methods / fields: `camelCase` — acronyms as words: `skyflowId` not `skyflowID`, `tokenUri` not `tokenURI` +- Methods / fields: `camelCase` — acronyms as words: `skyflowId` not `skyflowID`, `tokenUri` not `tokenURI`, `downloadUrl` not `downloadURL` - Constants: `UPPER_SNAKE_CASE` -- Builder methods: `setFooId()` not `setFooID()` +- Builder setter methods: `setFooId()` not `setFooID()` +- Deprecated methods must use `@Deprecated(since = "x.x", forRemoval = true)` + `@deprecated` Javadoc with `{@link}` to the replacement + +--- + +## 4. Response field normalisation -### 4. Response field normalisation - All response maps must use `skyflowId` (camelCase), never `skyflow_id` (snake_case) - `getErrors()` must be present on every response class -### 5. Test coverage +--- + +## 5. Test coverage + - Every public method must have at least one positive and one negative test - Tests must use `Assert.assertEquals` / `Assert.assertNull` — not just `Assert.fail` guards - No mocking of the production class under test +- Reflection-based tests on private methods are acceptable only when no public API exercises the method + +--- -### 6. Code quality -- No magic strings — use `Constants` or `ErrorMessage` enums -- No duplicate validation logic across request classes -- Methods over 40 lines are a smell — flag for decomposition +## 6. Code quality + +- No magic strings for API field names — use `Constants` or `ErrorMessage` enums +- No duplicate validation logic across request classes — belongs in `Validations` - No `@SuppressWarnings` without a comment explaining why +- `LogUtil.printWarningLog` must be used for deprecation warnings, not `System.err` + +--- + +## 7. Code smells + +Code smells are structural signals — they may not need immediate fixes but must be flagged. Report them at **Smell** severity. + +### Method & class size +- **Long method** — any method over 40 lines. Candidate for decomposition into private helpers. +- **Long class** — any class over 300 lines. May be taking on too many responsibilities. +- **Large parameter list** — more than 4 parameters on a method. Consider a config/options object. + +### Responsibility violations +- **Business logic in Request/Response classes** — these are data holders. If a Request/Response class contains conditional logic beyond null-safe getters, flag it. +- **toString() with business logic** — `toString()` should only serialise state. Logic like field renaming, manual JSON construction, or conditional field injection belongs in the controller or formatter methods. +- **Validation outside Validations.java** — any `if (x == null) throw new SkyflowException(...)` outside `src/main/java/com/skyflow/utils/validations/` is misplaced. + +### Control flow +- **Deep nesting** — more than 3 levels of `if`/`for`/`try` nesting. Extract inner blocks to named methods. +- **Long if-else chains** — more than 4 branches. Consider a map, switch, or polymorphism. +- **Null checks scattered** — multiple consecutive null guards that could be replaced with `Optional` or early return. + +### Data +- **Magic numbers** — literal integers or sizes (e.g. `25`, `3600`, `100`) without a named constant. Use `Constants`. +- **Raw HashMap chains** — `HashMap` passed through more than 2 method boundaries without a typed wrapper or comment explaining why. Flag for awareness; don't require a fix. +- **Temporary field** — a class field that is only set in certain code paths and `null` the rest of the time. Should be a local variable or method parameter instead. + +### Dead code +- **Unused private methods** — private methods with no callers. +- **Unused imports** — any `import` not referenced in the file. +- **Unreachable code** — code after `return`/`throw` in the same branch. +- **Commented-out code** — blocks of commented code without explanation. Remove or add a TODO with a ticket reference. + +### Comments +- **Explains what, not why** — a comment that restates what the code does (`// get the vault ID`) is noise. Only flag comments that explain the *what* without adding *why*. +- **Stale comment** — a comment that contradicts the current code (e.g. references a removed parameter or old method name). + +--- ## Output Format @@ -54,13 +112,24 @@ Group findings by file. For each file: ``` ### path/to/File.java -| Severity | Line | Finding | -|---|---|---| -| Critical | 42 | SkyflowException swallowed in catch block | -| Bug | 87 | skyflow_id not normalised to skyflowId | -| Quality | 103 | Magic string "records" — use Constants | +| Severity | Line | Finding | +|------------|------|------------------------------------------------------------| +| Critical | 42 | SkyflowException swallowed in catch block | +| Bug | 87 | skyflow_id not normalised to skyflowId | +| Quality | 103 | Magic string "records" — use Constants | +| Smell | 210 | toString() renames map keys — move to formatter method | +| Smell | 315 | Method is 58 lines — candidate for decomposition | ``` -Severities: **Critical** (data loss / silent failure) | **Bug** (wrong behaviour) | **Edge Case** (unhandled input) | **Quality** (maintainability) | **Smell** (minor style) - -End with a tech-debt summary table and a verdict: `APPROVE` / `APPROVE WITH FIXES` / `REQUEST CHANGES`. +**Severities:** +| Level | Meaning | +|---|---| +| **Critical** | Data loss, silent failure, security risk — must fix before merge | +| **Bug** | Wrong behaviour, incorrect output — must fix before merge | +| **Edge Case** | Unhandled input that will cause runtime failure — fix before merge | +| **Quality** | Maintainability issue, naming violation, missing pattern — fix before merge | +| **Smell** | Structural signal, technical debt — flag and track, fix when in the area | + +End with: +1. A tech-debt summary table grouped by category (Error handling / Naming / Smells / Tests) +2. A verdict: `APPROVE` / `APPROVE WITH FIXES` / `REQUEST CHANGES` From de2da9492d1c669f2a30077b0ae16c43035f4676 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 12:28:24 +0530 Subject: [PATCH 22/48] chore: add DEPRECATED_SKYFLOW_ID_KEY log entry to InfoLogs --- src/main/java/com/skyflow/logs/InfoLogs.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java index f71fc416..a260148b 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/src/main/java/com/skyflow/logs/InfoLogs.java @@ -95,7 +95,10 @@ public enum InfoLogs { GET_DETECT_RUN_TRIGGERED("Get detect run method triggered."), VALIDATE_GET_DETECT_RUN_REQUEST("Validating get detect run request."), REIDENTIFY_TEXT_SUCCESS("Text data re-identified."), - ; + + // 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."); + private final String log; From 938f566a619509c24b0123bc3c3168f10bb8d830 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 12:31:43 +0530 Subject: [PATCH 23/48] fix: restore skyflow_id key in Get/Query responses for v2 backward compat Both skyflow_id (deprecated) and skyflowId (new form) are now present in response maps simultaneously. WARN log emitted per record. --- .../skyflow/vault/controller/VaultController.java | 6 ++++-- .../vault/controller/VaultControllerTests.java | 13 ++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java index 30b6ec49..c844f0a8 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -131,7 +131,8 @@ private static synchronized HashMap getFormattedGetRecord(V1Fiel } if (getRecord.containsKey("skyflow_id")) { - getRecord.put("skyflowId", getRecord.remove("skyflow_id")); + getRecord.put("skyflowId", getRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); } return getRecord; @@ -155,7 +156,8 @@ private static synchronized HashMap getFormattedQueryRecord(V1Fi } if (queryRecord.containsKey("skyflow_id")) { - queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); + queryRecord.put("skyflowId", queryRecord.get("skyflow_id")); + LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); } return queryRecord; diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 2c2e8995..18f63ee8 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -202,8 +202,8 @@ public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { @SuppressWarnings("unchecked") HashMap result = (HashMap) method.invoke(null, record); - Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); - Assert.assertEquals("skyflowId should be present", "abc-123", result.get("skyflowId")); + Assert.assertEquals("skyflowId should be present (new form)", "abc-123", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "abc-123", result.get("skyflow_id")); Assert.assertEquals("other fields should be preserved", "John", result.get("name")); } @@ -219,14 +219,13 @@ public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { @SuppressWarnings("unchecked") HashMap result = (HashMap) method.invoke(null, record); - Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); - Assert.assertEquals("skyflowId should be present", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("skyflowId should be present (new form)", "xyz-456", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "xyz-456", result.get("skyflow_id")); Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); } @Test public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception { - // tokens branch: fields absent, tokens present Map tokens = new HashMap<>(); tokens.put("skyflow_id", "tok-789"); tokens.put("card_number", "tok-card-abc"); @@ -237,8 +236,8 @@ public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws @SuppressWarnings("unchecked") HashMap result = (HashMap) method.invoke(null, record); - Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); - Assert.assertEquals("skyflowId should be present", "tok-789", result.get("skyflowId")); + Assert.assertEquals("skyflowId should be present (new form)", "tok-789", result.get("skyflowId")); + Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "tok-789", result.get("skyflow_id")); Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); } From 4c3b99823e4f8c623cf7dfd44210fff4b5630e8b Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 12:33:39 +0530 Subject: [PATCH 24/48] docs: add deprecation Javadoc for skyflow_id key in GetResponse and QueryResponse --- src/main/java/com/skyflow/vault/data/GetResponse.java | 8 ++++++++ src/main/java/com/skyflow/vault/data/QueryResponse.java | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/java/com/skyflow/vault/data/GetResponse.java b/src/main/java/com/skyflow/vault/data/GetResponse.java index 34a01303..365fe38e 100644 --- a/src/main/java/com/skyflow/vault/data/GetResponse.java +++ b/src/main/java/com/skyflow/vault/data/GetResponse.java @@ -14,6 +14,14 @@ 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/src/main/java/com/skyflow/vault/data/QueryResponse.java b/src/main/java/com/skyflow/vault/data/QueryResponse.java index 9a6b6804..afb32c60 100644 --- a/src/main/java/com/skyflow/vault/data/QueryResponse.java +++ b/src/main/java/com/skyflow/vault/data/QueryResponse.java @@ -18,6 +18,14 @@ public QueryResponse(ArrayList> 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; } From 8a1744a47dda8df829c12123c71437f4e1171590 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 12:38:27 +0530 Subject: [PATCH 25/48] feat: deprecate downloadURL in favour of downloadUrl in GetRequest and DetokenizeRequest Old downloadURL() methods kept as @Deprecated(forRemoval=true) delegates. Runtime WARN log emitted on old form usage. 100% test coverage: new form, deprecated form, default value for both classes. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/main/java/com/skyflow/VaultClient.java | 2 +- src/main/java/com/skyflow/logs/InfoLogs.java | 3 +- .../vault/controller/VaultController.java | 2 +- .../com/skyflow/vault/data/GetRequest.java | 28 +++++++++-- .../vault/tokens/DetokenizeRequest.java | 29 +++++++++-- .../controller/VaultControllerTests.java | 50 +++++++++++++++++++ 6 files changed, 103 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/skyflow/VaultClient.java b/src/main/java/com/skyflow/VaultClient.java index e3352f19..76de35e8 100644 --- a/src/main/java/com/skyflow/VaultClient.java +++ b/src/main/java/com/skyflow/VaultClient.java @@ -122,7 +122,7 @@ protected V1DetokenizePayload getDetokenizePayload(DetokenizeRequest request) { return V1DetokenizePayload.builder() .continueOnError(request.getContinueOnError()) - .downloadUrl(request.getDownloadURL()) + .downloadUrl(request.getDownloadUrl()) .detokenizationParameters(recordRequests) .build(); } diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java index a260148b..6d745b9e 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/src/main/java/com/skyflow/logs/InfoLogs.java @@ -97,7 +97,8 @@ public enum InfoLogs { REIDENTIFY_TEXT_SUCCESS("Text data re-identified."), // 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_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead."), + DEPRECATED_DOWNLOAD_URL("[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead."); diff --git a/src/main/java/com/skyflow/vault/controller/VaultController.java b/src/main/java/com/skyflow/vault/controller/VaultController.java index c844f0a8..35f3a798 100644 --- a/src/main/java/com/skyflow/vault/controller/VaultController.java +++ b/src/main/java/com/skyflow/vault/controller/VaultController.java @@ -291,7 +291,7 @@ public GetResponse get(GetRequest getRequest) throws SkyflowException { .tokenization(getRequest.getReturnTokens()) .offset(getRequest.getOffset()) .limit(getRequest.getLimit()) - .downloadUrl(getRequest.getDownloadURL()) + .downloadUrl(getRequest.getDownloadUrl()) .columnName(getRequest.getColumnName()) .columnValues(getRequest.getColumnValues()) .fields(getRequest.getFields()) diff --git a/src/main/java/com/skyflow/vault/data/GetRequest.java b/src/main/java/com/skyflow/vault/data/GetRequest.java index 04626e35..0fccf7b8 100644 --- a/src/main/java/com/skyflow/vault/data/GetRequest.java +++ b/src/main/java/com/skyflow/vault/data/GetRequest.java @@ -1,7 +1,9 @@ package com.skyflow.vault.data; import com.skyflow.enums.RedactionType; +import com.skyflow.logs.InfoLogs; import com.skyflow.utils.Constants; +import com.skyflow.utils.logger.LogUtil; import java.util.ArrayList; @@ -44,8 +46,17 @@ public String getLimit() { return this.builder.limit; } + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { - return this.builder.downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; } public String getColumnName() { @@ -68,14 +79,14 @@ public static final class GetRequestBuilder { private ArrayList fields; private String offset; private String limit; - private Boolean downloadURL; + private Boolean downloadUrl; private String columnName; private ArrayList columnValues; private String orderBy; private GetRequestBuilder() { - this.downloadURL = true; this.orderBy = Constants.ORDER_ASCENDING; + this.downloadUrl = true; } public GetRequestBuilder table(String table) { @@ -113,8 +124,17 @@ public GetRequestBuilder limit(String limit) { return this; } + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public GetRequestBuilder downloadURL(Boolean downloadURL) { - this.downloadURL = downloadURL == null || downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return downloadUrl(downloadURL); + } + + public GetRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl == null || downloadUrl; return this; } diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 186a18d2..481c0c16 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -1,5 +1,8 @@ package com.skyflow.vault.tokens; +import com.skyflow.logs.InfoLogs; +import com.skyflow.utils.logger.LogUtil; + import java.util.ArrayList; public class DetokenizeRequest { @@ -21,18 +24,27 @@ public Boolean getContinueOnError() { return this.builder.continueOnError; } + /** + * @deprecated Use {@link #getDownloadUrl()} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public Boolean getDownloadURL() { - return this.builder.downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return getDownloadUrl(); + } + + public Boolean getDownloadUrl() { + return this.builder.downloadUrl; } public static final class DetokenizeRequestBuilder { private ArrayList detokenizeData; private Boolean continueOnError; - private Boolean downloadURL; + private Boolean downloadUrl; private DetokenizeRequestBuilder() { this.continueOnError = false; - this.downloadURL = false; + this.downloadUrl = false; } public DetokenizeRequestBuilder detokenizeData(ArrayList detokenizeData) { @@ -45,8 +57,17 @@ public DetokenizeRequestBuilder continueOnError(Boolean continueOnError) { return this; } + /** + * @deprecated Use {@link #downloadUrl(Boolean)} instead. + */ + @Deprecated(since = "2.1", forRemoval = true) public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { - this.downloadURL = downloadURL; + LogUtil.printWarningLog(InfoLogs.DEPRECATED_DOWNLOAD_URL.getLog()); + return downloadUrl(downloadURL); + } + + public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { + this.downloadUrl = downloadUrl; return this; } diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 18f63ee8..5dc02f37 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -241,4 +241,54 @@ public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); } + @Test + public void testGetRequestDownloadUrlNewForm() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadUrl(true) + .build(); + Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl()); + } + + @Test + public void testGetRequestDownloadURLDeprecatedFormStillWorks() { + GetRequest request = GetRequest.builder() + .table("test_table") + .downloadURL(true) + .build(); + Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL()); + Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl()); + } + + @Test + public void testGetRequestDownloadUrlDefaultIsTrue() { + GetRequest request = GetRequest.builder() + .table("test_table") + .build(); + Assert.assertTrue("downloadUrl should be true by default (preserved from original)", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadUrlNewForm() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadUrl(true) + .build(); + Assert.assertTrue("new downloadUrl(true) should be set", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadURLDeprecatedFormStillWorks() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadURL(true) + .build(); + Assert.assertTrue("deprecated downloadURL() should still work", request.getDownloadURL()); + Assert.assertTrue("new getDownloadUrl() returns same value", request.getDownloadUrl()); + } + + @Test + public void testDetokenizeRequestDownloadUrlDefaultIsFalse() { + DetokenizeRequest request = DetokenizeRequest.builder().build(); + Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl()); + } + } From 92bcf3391116f4fec4e1ffbcf687b32c834db9ab Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:30:04 +0530 Subject: [PATCH 26/48] =?UTF-8?q?chore:=20update=20CLAUDE.md=20=E2=80=94?= =?UTF-8?q?=20add=20code-smell=20command,=20update=20slash=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 30753abc..10288ad7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,7 +100,8 @@ See `docs/superpowers/specs/` for in-progress design specs and `docs/superpowers ## Slash Commands -- `/code-review` — code review against SDK patterns (see `.claude/commands/code-review.md`) -- `/code-security` — security audit (see `.claude/commands/code-security.md`) +- `/code-review` — full review: SDK patterns + code smells + security checks (reads `.claude/commands/code-smell.md` and `.claude/commands/code-security.md` inline) +- `/code-smell` — standalone structural smell analysis only (long methods, dead code, misplaced logic) +- `/code-security` — standalone security audit only (credentials, input validation, HTTP security) - `/sdk-sample ` — generate a sample file for a feature - `/test [ClassName]` — run quality pipeline (compile → checkstyle → build → test → coverage) From dbdcc073301739510337f084043be927cdc658cb Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:31:00 +0530 Subject: [PATCH 27/48] docs: update deprecation plan and PM doc - credentials permanently supported Co-Authored-By: Claude Sonnet 4.6 (1M context) --- ...-14-v2-backward-compatibility-deprecation.md | 14 +++++--------- docs/v2-public-interface-changes.md | 17 ++++++----------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md index 2056f2a9..51b0562c 100644 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md @@ -17,8 +17,8 @@ | Change | Status | Fix needed | |---|---|---| | `skyflow_id` removed from Get/Query response maps | **BREAKING** | Keep both `skyflow_id` + `skyflowId`; emit WARN | -| `clientID`/`keyID`/`tokenURI` replaced in BearerToken | Not breaking (fallback exists) | Add WARN log on old-form fallback | -| `clientID`/`keyID` replaced in SignedDataTokens | Not breaking (fallback exists) | Add WARN log on old-form fallback | +| `clientID`/`keyID`/`tokenURI` in BearerToken | Not breaking — both forms supported permanently | No action needed | +| `clientID`/`keyID` in SignedDataTokens | Not breaking — both forms supported permanently | No action needed | | `getErrors()` added to QueryResponse | Not breaking (additive) | No change needed | | `downloadURL` → `downloadUrl` in GetRequest & DetokenizeRequest | **BREAKING** | Keep `@Deprecated` old methods; add new `downloadUrl` methods | @@ -28,17 +28,13 @@ | File | Change | |---|---| -| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 5 deprecation warning log entries (4 existing + 1 for downloadURL) | -| `src/main/java/com/skyflow/vault/data/GetRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` | -| `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` | +| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 2 deprecation warning log entries (`skyflow_id` key + `downloadURL` method) | +| `src/main/java/com/skyflow/vault/data/GetRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` + WARN log | +| `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` + WARN log | | `src/main/java/com/skyflow/vault/controller/VaultController.java` | Keep `skyflow_id` key alongside `skyflowId`; emit WARN per record | | `src/main/java/com/skyflow/vault/data/GetResponse.java` | Add `@deprecated` Javadoc on `getData()` for `skyflow_id` key | | `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `@deprecated` Javadoc on `getFields()` for `skyflow_id` key | -| `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java` | Add WARN log when `clientID`/`keyID`/`tokenURI` fallback fires | -| `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java` | Add WARN log when `clientID`/`keyID` fallback fires | | `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` | Update existing tests: assert BOTH `skyflow_id` and `skyflowId` present | -| `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` | Existing tests unchanged (old-form already passes); add comment | -| `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` | Existing tests unchanged | --- diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md index d9aa9e34..feb14413 100644 --- a/docs/v2-public-interface-changes.md +++ b/docs/v2-public-interface-changes.md @@ -8,9 +8,9 @@ ## Overview -As part of aligning the Skyflow Java SDK with cross-language naming standards, a set of public-facing field names and response keys are being updated. All changes are designed to be **non-breaking for existing customers** — old forms continue to work alongside new ones, with deprecation warnings logged at runtime to guide migration. +As part of aligning the Skyflow Java SDK with cross-language naming standards, a set of public-facing field names and response keys are being updated. All changes are designed to be **non-breaking for existing customers** — old forms continue to work alongside new ones. -A future release will remove the deprecated forms entirely. No removal date is set yet. +Where applicable, deprecation warnings are logged at runtime or signalled at compile time to guide migration. Credential JSON field names (`clientID`, `keyID`, `tokenURI`) are permanently supported alongside the new forms — no migration required. --- @@ -37,9 +37,9 @@ Hovering over the deprecated method shows an inline tooltip: If a customer selects the deprecated form and uses it in their code, the IDE shows an **orange underline** at the call site — a stronger visual than a plain yellow warning — because the method is marked `forRemoval = true`. -### Runtime log warnings (credential fields, `skyflow_id` key) +### Runtime log warnings (`skyflow_id` key) -For changes that cannot use Java annotations (map keys, JSON field names), a `[DEPRECATED]` warning is logged at runtime when the old form is used: +For map key changes that cannot use Java annotations, a `[DEPRECATED]` warning is logged at runtime when the old key is accessed: ``` [DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed @@ -58,18 +58,13 @@ These appear in the application log at WARN level. Customers running with `LogLe When customers authenticate using a service account credentials JSON file, the field names inside that file are changing to follow Java naming conventions (lowercase acronyms). -| Old field name (deprecated) | New field name | Used in | +| Old field name | New field name | Used in | |---|---|---| | `clientID` | `clientId` | `credentials.json` | | `keyID` | `keyId` | `credentials.json` | | `tokenURI` | `tokenUri` | `credentials.json` | -**Customer impact:** Customers with existing credentials files using the old field names (`clientID`, `keyID`, `tokenURI`) will continue to work without any changes. A deprecation warning will appear in their application logs recommending they update to the new field names. - -**Example of the warning customers will see in their logs:** -``` -[DEPRECATED] Credential field 'clientID' is deprecated and will be removed in an upcoming release. Use 'clientId' instead. -``` +**Customer impact:** Both old and new field names are permanently supported — existing credentials files require no changes. No deprecation warning is emitted. Customers may migrate to the new names at any time but are not required to. --- From 5b578577cd08c0d603e84acb686dda7ff2fd5f93 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:31:11 +0530 Subject: [PATCH 28/48] test: add new-form downloadUrl tests alongside deprecated downloadURL tests Both old (downloadURL) and new (downloadUrl) builder methods tested: - GetTests: 2 new tests for downloadUrl() with cross-assertion on deprecated getDownloadURL() returning same value - DetokenizeTests: 1 new test same pattern - VaultClientTests: 1 new integration test for DetokenizeRequest.downloadUrl() Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../java/com/skyflow/VaultClientTests.java | 20 +++++++++++ .../java/com/skyflow/vault/data/GetTests.java | 36 +++++++++++++++++++ .../skyflow/vault/tokens/DetokenizeTests.java | 16 +++++++++ 3 files changed, 72 insertions(+) diff --git a/src/test/java/com/skyflow/VaultClientTests.java b/src/test/java/com/skyflow/VaultClientTests.java index 4c9be65f..f3e9f738 100644 --- a/src/test/java/com/skyflow/VaultClientTests.java +++ b/src/test/java/com/skyflow/VaultClientTests.java @@ -168,6 +168,26 @@ public void testGetDetokenizePayload() { } } + @Test + public void testGetDetokenizePayloadWithNewDownloadUrl() { + try { + DetokenizeData detokenizeDataRecord1 = new DetokenizeData(token); + detokenizeData.clear(); + detokenizeData.add(detokenizeDataRecord1); + DetokenizeRequest detokenizeRequest = DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .downloadUrl(true) // new form + .continueOnError(false) + .build(); + V1DetokenizePayload payload = vaultClient.getDetokenizePayload(detokenizeRequest); + Assert.assertTrue("new downloadUrl() should be reflected in payload", payload.getDownloadUrl().get()); + Assert.assertTrue("new getDownloadUrl() should return true", detokenizeRequest.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", detokenizeRequest.getDownloadURL()); + } catch (Exception e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testGetBulkInsertRequestBody() { try { diff --git a/src/test/java/com/skyflow/vault/data/GetTests.java b/src/test/java/com/skyflow/vault/data/GetTests.java index 74306d4b..e62c605f 100644 --- a/src/test/java/com/skyflow/vault/data/GetTests.java +++ b/src/test/java/com/skyflow/vault/data/GetTests.java @@ -97,6 +97,24 @@ public void testValidGetByIdInputInGetRequestValidations() { } } + @Test + public void testValidGetByIdInputWithNewDownloadUrl() { + try { + ids.add(skyflowID); + fields.add(field); + GetRequest request = GetRequest.builder() + .ids(ids) + .table(table) + .downloadUrl(false) // new form + .build(); + Validations.validateGetRequest(request); + Assert.assertFalse("new getDownloadUrl() should return false", request.getDownloadUrl()); + Assert.assertFalse("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testValidGetByColumnValuesInputInGetRequestValidations() { try { @@ -129,6 +147,24 @@ public void testValidGetByColumnValuesInputInGetRequestValidations() { } } + @Test + public void testValidGetByColumnValuesInputWithNewDownloadUrl() { + try { + columnValues.add(columnValue); + GetRequest request = GetRequest.builder() + .table(table) + .columnName(columnName) + .columnValues(columnValues) + .downloadUrl(true) // new form + .build(); + Validations.validateGetRequest(request); + Assert.assertTrue("new getDownloadUrl() should return true", request.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testNoTableInGetRequestValidations() { ids.add(skyflowID); diff --git a/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java b/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java index d417aeb7..43aad475 100644 --- a/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java +++ b/src/test/java/com/skyflow/vault/tokens/DetokenizeTests.java @@ -52,6 +52,22 @@ public void testValidInputInDetokenizeRequestValidations() { } } + @Test + public void testValidInputWithNewDownloadUrlInDetokenizeRequestValidations() { + try { + detokenizeData.add(maskedRedactionRecord); + DetokenizeRequest request = DetokenizeRequest.builder() + .detokenizeData(detokenizeData) + .downloadUrl(true) // new form + .build(); + Validations.validateDetokenizeRequest(request); + Assert.assertTrue("new getDownloadUrl() should return true", request.getDownloadUrl()); + Assert.assertTrue("deprecated getDownloadURL() should return same value", request.getDownloadURL()); + } catch (SkyflowException e) { + Assert.fail(INVALID_EXCEPTION_THROWN); + } + } + @Test public void testNoTokensInDetokenizeRequestValidations() { DetokenizeRequest request = DetokenizeRequest.builder().build(); From 008f4ba66add0a985f04d38a25ba75fe1f62c60c Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:41:47 +0530 Subject: [PATCH 29/48] fix: changes to claude --- CLAUDE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 10288ad7..1e1d966b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,13 @@ +--- +name: skyflow-java-sdk +description: Skyflow Java SDK project context — naming conventions, build commands, known failures, and slash commands. Loaded for all Java source, test, and sample files. +paths: + - src/**/*.java + - samples/**/*.java + - pom.xml + - checkstyle.xml +--- + # Skyflow Java SDK — Claude Code Instructions ## Project Overview From b9f0e007ea4173baf82451293992a46bc7c926d0 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:56:21 +0530 Subject: [PATCH 30/48] docs: migrate V1-to-V2 guide from README to docs/, update CHANGELOG - Extract 260-line migration section from README.md to docs/migrate_to_v2.md following the pattern established in skyflow-node PR #258 - README now links to docs/migrate_to_v2.md instead of inline content - docs/migrate_to_v2.md adds v2.1+ sections for credential field renames and skyflow_id deprecation (new content) - CHANGELOG.md: add v2.0.4 release notes covering nomenclature changes, backward compat deprecations, and validation removal Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 18 +++ README.md | 267 +----------------------------------------- docs/migrate_to_v2.md | 252 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 265 deletions(-) create mode 100644 docs/migrate_to_v2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 10606856..c663a53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Changelog All notable changes to this project will be documented in this file. +## [2.0.4] - 2025-05-15 +### Changed +- Credential JSON field names `clientID`, `keyID`, `tokenURI` renamed to `clientId`, `keyId`, `tokenUri` (Java camelCase convention). Both old and new forms permanently accepted — no migration required. +- Response maps now return `skyflowId` (camelCase) for Get and Query operations. Legacy `skyflow_id` key retained alongside for backward compatibility; deprecated and will be removed in an upcoming release. +- `GetRequest` and `DetokenizeRequest`: added `downloadUrl()` / `getDownloadUrl()` methods following acronym-as-word convention. Old `downloadURL()` / `getDownloadURL()` kept as `@Deprecated` delegates. +- `QueryResponse`: added `getErrors()` accessor (was missing; all other response classes already had it). +- Removed SDK-level null/empty field value validation from Insert and Update — backend is authoritative per API spec (`additionalProperties: Any type`). + +## [2.0.3] - 2025-04-01 +### Added +- Initial stable v2 release with builder pattern for all request types. +- Multi-vault support via `Skyflow.builder().addVaultConfig()`. +- Per-client log level configuration. +- Service account authentication: bearer token and signed data token generation. +- Vault operations: Insert, Get, Update, Delete, Query, Tokenize, Detokenize, File Upload. +- Detect API: Deidentify/Reidentify text and file. +- Connections: Invoke connection. + ## [1.15.0] - 2024-08-01 ### Added - insert data using bulk operation `insertBulk` diff --git a/README.md b/README.md index 3f8a9adb..e97c545d 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,7 @@ The Skyflow Java SDK is designed to help with integrating Skyflow into a Java ba - [Configuration](#configuration) - [Gradle users](#gradle-users) - [Maven users](#maven-users) -- [Migration from v1 to v2](#migration-from-v1-to-v2) - - [Authentication options](#authentication-options) - - [Initializing the client](#initializing-the-client) - - [Request & response structure](#request--response-structure) - - [Request options](#request-options) - - [Error structure](#error-structure) +- [Migration from v1 to v2](docs/migrate_to_v2.md) - [Quickstart](#quickstart) - [Authenticate](#authenticate) - [Initialize the client](#initialize-the-client) @@ -94,265 +89,7 @@ Add this dependency to your project's `pom.xml` file: # Migrate from v1 to v2 -Below are the steps to migrate the java sdk from v1 to v2. - -### Authentication options - -In V2, we have introduced multiple authentication options. You can now provide credentials in the following ways: - -- Passing credentials in ENV. (`SKYFLOW_CREDENTIALS`) _(Recommended)_ -- API Key -- Path to your credentials JSON file -- Stringified JSON of your credentials -- Bearer token - -These options allow you to choose the authentication method that best suits your use case. - -**V1 (Old)** - -```java -static class DemoTokenProvider implements TokenProvider { - @Override - public String getBearerToken() throws Exception { - ResponseToken res = null; - try { - String filePath = ""; - res = Token.generateBearerToken(filePath); - } catch (SkyflowException e) { - e.printStackTrace(); - } - return res.getAccessToken(); - } -} -``` - -**V2 (New): Passing one of the following:** - -```java -// Option 1: API Key (Recommended) -Credentials skyflowCredentials = new Credentials(); -skyflowCredentials.setApiKey(""); // Replace with your actual API key - -// Option 2: Environment Variables (Recommended) -// Set SKYFLOW_CREDENTIALS in your environment - -// Option 3: Credentials File -skyflowCredentials.setPath(""); // Replace with the path to credentials file - -// Option 4: Stringified JSON -skyflowCredentials.setCredentialsString(""); // Replace with the credentials string - -// Option 5: Bearer Token -skyflowCredentials.setToken(""); // Replace with your actual authentication token. -``` - -Notes: - -- Use only ONE authentication method. -- API Key or environment variables are recommended for production use. -- Secure storage of credentials is essential. -- For overriding behavior and priority order of credentials, please refer to [Initialize the client](#initialize-the-client) section in [Quickstart](#quickstart). - ---- - -### Initializing the client - -In V2, we have introduced a builder design pattern for client initialization and added support for multi-vault. This allows you to configure multiple vaults during client initialization. In V2, the log level is tied to each individual client instance. During client initialization, you can pass the following parameters: - -- `vaultId` and `clusterId`: These values are derived from the vault ID & vault URL. -- `env`: Specify the environment (e.g., SANDBOX or PROD). -- `credentials`: The necessary authentication credentials. - -**V1 (Old)** - -```java -// DemoTokenProvider class is an implementation of the TokenProvider interface -DemoTokenProvider demoTokenProvider = new DemoTokenProvider(); -SkyflowConfiguration skyflowConfig = new SkyflowConfiguration("","", demoTokenProvider); -Skyflow skyflowClient = Skyflow.init(skyflowConfig); -``` - -**V2 (New)** - -```java -Credentials credentials = new Credentials(); -credentials.setPath(""); // Replace with the path to the credentials file - -// Configure the first vault (Blitz) -VaultConfig config = new VaultConfig(); -config.setVaultId(""); // Replace with the ID of the first vault -config.setClusterId(""); // Replace with the cluster ID of the first vault -config.setEnv(Env.DEV); // Set the environment (e.g., DEV, STAGE, PROD) -config.setCredentials(credentials); // Associate the credentials with the vault - -// Set up credentials for the Skyflow client -Credentials skyflowCredentials = new Credentials(); -skyflowCredentials.setPath(""); // Replace with the path to another credentials file - -// Create a Skyflow client and add vault configurations -Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.DEBUG) // Enable debugging for detailed logs - .addVaultConfig(config) // Add the first vault configuration - .addSkyflowCredentials(skyflowCredentials) // Add general Skyflow credentials - .build(); -``` - -**Key Changes:** - -- `vaultUrl` replaced with `clusterId`. -- Added environment specification (`env`). -- Instance-specific log levels. - ---- - -### Request & response structure - -In V2, we have removed the use of JSON objects from a third-party package. Instead, we have transitioned to accepting native ArrayList and HashMap data structures and adopted the builder pattern for request creation. This request needs: - -- `table`: The name of the table. -- `values`: An array list of objects containing the data to be inserted. - -The response will be of type `InsertResponse` class, which contains `insertedFields` and `errors`. - -**V1 (Old):** Request building - -```java -JSONObject recordsJson = new JSONObject(); -JSONArray recordsArrayJson = new JSONArray(); - -JSONObject recordJson = new JSONObject(); -recordJson.put("table", "cards"); - -JSONObject fieldsJson = new JSONObject(); -fields.put("cardNumber", "41111111111"); -fields.put("cvv", "123"); - -recordJson.put("fields", fieldsJson); -recordsArrayJson.add(record); -recordsJson.put("records", recordsArrayJson); -try { - JSONObject insertResponse = skyflowClient.insert(records); - System.out.println(insertResponse); -} catch (SkyflowException exception) { - System.out.println(exception); -} -``` - -**V2 (New):** Request building - -```java -ArrayList> values = new ArrayList<>(); -HashMap value = new HashMap<>(); -value.put("", ""); // Replace with column name and value -value.put("", ""); // Replace with another column name and value -values.add(values); - -ArrayList> tokens = new ArrayList<>(); -HashMap token = new HashMap<>(); -token.put("", ""); // Replace with the token for COLUMN_NAME_2 -tokens.add(token); - -InsertRequest insertRequest = InsertRequest.builder() - .table("") // Replace with the table name - .continueOnError(true) // Continue inserting even if some records fail - .tokenMode(TokenMode.ENABLE) // Enable BYOT for token validation - .values(values) // Data to insert - .tokens(tokens) // Provide tokens for BYOT columns - .returnTokens(true) // Return tokens along with the response - .build(); -``` - -**V1 (Old):** Response structure - -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f", - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - } - ] -} -``` - -**V2 (New):** Response structure - -```json -{ - "insertedFields": [ - { - "card_number": "5484-7829-1702-9110", - "request_index": "0", - "skyflow_id": "9fac9201-7b8a-4446-93f8-5244e1213bd1", - "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" - } - ], - "errors": [] -} -``` - ---- - -### Request options - -In V2, with the introduction of the builder design pattern has made handling optional fields in Java more efficient and straightforward. - -**V1 (Old)** - -```java -InsertOptions insertOptions = new InsertOptions(true); -``` - -**V2 (New)** - -```java -InsertRequest upsertRequest = InsertRequest.builder() - .table("") // Replace with the table name - .continueOnError(false) // Stop inserting if any record fails - .tokenMode(TokenMode.DISABLE) // Disable BYOT - .values(values) // Data to insert - .returnTokens(false) // Do not return tokens - .upsert("") // Replace with the column name used for upsert logic - .build(); -``` - ---- - -### Error structure - -In V2, we have enriched the error details to provide better debugging capabilities. -The error response now includes: - -- `httpStatus`: The HTTP status code. -- `grpcCode`: The gRPC code associated with the error. -- `details` & `message`: A detailed description of the error. -- `requestId`: A unique request identifier for easier debugging. - -**V1 (Old):** Error structure - -```json -{ - "code": "", - "description": "" -} -``` - -**V2 (New):** Error structure - -```js -{ - "httpStatus": "", - "grpcCode": , - "httpCode": , - "message": "", - "requestId": "", - "details": ["
"] -} -``` +Upgrading from v1? See the dedicated migration guide: **[docs/migrate_to_v2.md](docs/migrate_to_v2.md)** # Quickstart diff --git a/docs/migrate_to_v2.md b/docs/migrate_to_v2.md new file mode 100644 index 00000000..55facd45 --- /dev/null +++ b/docs/migrate_to_v2.md @@ -0,0 +1,252 @@ +# Skyflow Java SDK — V1 to V2 Migration Guide + +This guide covers the steps to migrate the Skyflow Java SDK from v1 to v2. + +--- + +## Authentication options + +In V2, multiple authentication options are available. You can now provide credentials in the following ways: + +- Environment variable (`SKYFLOW_CREDENTIALS`) _(Recommended)_ +- API Key +- Path to credentials JSON file +- Stringified JSON of credentials +- Bearer token + +**V1 (Old)** + +```java +static class DemoTokenProvider implements TokenProvider { + @Override + public String getBearerToken() throws Exception { + ResponseToken res = null; + try { + String filePath = ""; + res = Token.generateBearerToken(filePath); + } catch (SkyflowException e) { + e.printStackTrace(); + } + return res.getAccessToken(); + } +} +``` + +**V2 (New): Choose one of the following:** + +```java +// Option 1: API Key (Recommended) +Credentials skyflowCredentials = new Credentials(); +skyflowCredentials.setApiKey(""); + +// Option 2: Environment Variable (Recommended) +// Set SKYFLOW_CREDENTIALS in your environment + +// Option 3: Credentials File +skyflowCredentials.setPath(""); + +// Option 4: Stringified JSON +skyflowCredentials.setCredentialsString(""); + +// Option 5: Bearer Token +skyflowCredentials.setToken(""); +``` + +> **Notes:** +> - Use only ONE authentication method per credentials object. +> - API Key or environment variable are recommended for production. +> - For priority order see [Quickstart — Initialize the client](../README.md#initialize-the-client). + +--- + +## Initializing the client + +V2 introduces a builder pattern for client initialization with multi-vault support. + +**Key changes:** +- `vaultUrl` replaced with `clusterId` (derived from vault URL) +- Added `env` specification (e.g. `Env.PROD`, `Env.SANDBOX`) +- Log level is now per-client-instance + +**V1 (Old)** + +```java +DemoTokenProvider demoTokenProvider = new DemoTokenProvider(); +SkyflowConfiguration skyflowConfig = new SkyflowConfiguration( + "", "", demoTokenProvider +); +Skyflow skyflowClient = Skyflow.init(skyflowConfig); +``` + +**V2 (New)** + +```java +Credentials credentials = new Credentials(); +credentials.setPath(""); + +VaultConfig config = new VaultConfig(); +config.setVaultId(""); +config.setClusterId(""); +config.setEnv(Env.PROD); +config.setCredentials(credentials); + +Skyflow skyflowClient = Skyflow.builder() + .setLogLevel(LogLevel.DEBUG) + .addVaultConfig(config) + .build(); +``` + +--- + +## Request and response structure + +V2 removes third-party JSON objects in favour of native `ArrayList` and `HashMap` with a builder pattern for requests. + +**V1 (Old) — Request** + +```java +JSONObject recordsJson = new JSONObject(); +JSONArray recordsArrayJson = new JSONArray(); +JSONObject recordJson = new JSONObject(); +recordJson.put("table", "cards"); +JSONObject fieldsJson = new JSONObject(); +fieldsJson.put("cardNumber", "41111111111"); +fieldsJson.put("cvv", "123"); +recordJson.put("fields", fieldsJson); +recordsArrayJson.add(recordJson); +recordsJson.put("records", recordsArrayJson); +try { + JSONObject insertResponse = skyflowClient.insert(records); +} catch (SkyflowException e) { + System.out.println(e); +} +``` + +**V2 (New) — Request** + +```java +HashMap value = new HashMap<>(); +value.put("", ""); +value.put("", ""); +ArrayList> values = new ArrayList<>(); +values.add(value); + +InsertRequest insertRequest = InsertRequest.builder() + .table("") + .values(values) + .returnTokens(true) + .build(); + +InsertResponse response = skyflowClient.vault().insert(insertRequest); +``` + +**V1 (Old) — Response** + +```json +{ + "records": [ + { + "table": "cards", + "fields": { + "skyflow_id": "16419435-aa63-4823-aae7-19c6a2d6a19f", + "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + "cvv": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" + } + } + ] +} +``` + +**V2 (New) — Response** + +```json +{ + "insertedFields": [ + { + "skyflowId": "9fac9201-7b8a-4446-93f8-5244e1213bd1", + "card_number": "5484-7829-1702-9110", + "cardholder_name": "b2308e2a-c1f5-469b-97b7-1f193159399b" + } + ], + "errors": null +} +``` + +--- + +## Request options + +V2 builder pattern replaces V1 options objects. + +**V1 (Old)** + +```java +InsertOptions insertOptions = new InsertOptions(true); +``` + +**V2 (New)** + +```java +InsertRequest request = InsertRequest.builder() + .table("") + .values(values) + .continueOnError(false) + .tokenMode(TokenMode.DISABLE) + .returnTokens(false) + .upsert("") + .build(); +``` + +--- + +## Error structure + +V2 provides richer error details for easier debugging. + +**V1 (Old)** + +```json +{ + "code": "", + "description": "" +} +``` + +**V2 (New)** + +```json +{ + "httpStatus": "", + "grpcCode": "", + "httpCode": "", + "message": "", + "requestId": "", + "details": ["
"] +} +``` + +--- + +## Credential field names (v2.1+) + +The credentials JSON file field names are updated to follow Java camelCase conventions. Both old and new forms are permanently accepted. + +| Old form (still accepted) | New form (preferred) | +|---|---| +| `clientID` | `clientId` | +| `keyID` | `keyId` | +| `tokenURI` | `tokenUri` | + +--- + +## Response field names (v2.1+) + +Response maps now return `skyflowId` (camelCase). The legacy `skyflow_id` key is still present for backward compatibility but is deprecated. + +| Deprecated (still returned) | Preferred | +|---|---| +| `skyflow_id` | `skyflowId` | + +--- + +For the full list of changes see [CHANGELOG.md](../CHANGELOG.md). From fa7a0fcdc2c3f9f5fa7ee73f3d19696658746ab2 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:58:24 +0530 Subject: [PATCH 31/48] =?UTF-8?q?docs:=20simplify=20CHANGELOG=20=E2=80=94?= =?UTF-8?q?=20remove=20v1=20entries,=20keep=20only=20v2.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 122 +++------------------------------------------------ 1 file changed, 6 insertions(+), 116 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c663a53c..d1e3390d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,121 +1,11 @@ # Changelog + All notable changes to this project will be documented in this file. ## [2.0.4] - 2025-05-15 ### Changed -- Credential JSON field names `clientID`, `keyID`, `tokenURI` renamed to `clientId`, `keyId`, `tokenUri` (Java camelCase convention). Both old and new forms permanently accepted — no migration required. -- Response maps now return `skyflowId` (camelCase) for Get and Query operations. Legacy `skyflow_id` key retained alongside for backward compatibility; deprecated and will be removed in an upcoming release. -- `GetRequest` and `DetokenizeRequest`: added `downloadUrl()` / `getDownloadUrl()` methods following acronym-as-word convention. Old `downloadURL()` / `getDownloadURL()` kept as `@Deprecated` delegates. -- `QueryResponse`: added `getErrors()` accessor (was missing; all other response classes already had it). -- Removed SDK-level null/empty field value validation from Insert and Update — backend is authoritative per API spec (`additionalProperties: Any type`). - -## [2.0.3] - 2025-04-01 -### Added -- Initial stable v2 release with builder pattern for all request types. -- Multi-vault support via `Skyflow.builder().addVaultConfig()`. -- Per-client log level configuration. -- Service account authentication: bearer token and signed data token generation. -- Vault operations: Insert, Get, Update, Delete, Query, Tokenize, Detokenize, File Upload. -- Detect API: Deidentify/Reidentify text and file. -- Connections: Invoke connection. - -## [1.15.0] - 2024-08-01 -### Added -- insert data using bulk operation `insertBulk` - -## [1.14.0] - 2024-02-01 -### Fixed -- handling of detokenize response to avoid breaking changes. - -## [1.13.0] - 2024-01-10 -### Added -- Continue on error support for batch Insert. - -## [1.12.1] - 2023-11-09 -### Fixed -- Static Bearer token being used for multiple Skyflow Client instances. - -## [1.12.0] - 2023-10-25 -### Added -- `tokens` support in Get Method - -## [1.11.0] - 2023-09-01 -### Added -- `query` vault API - -## [1.10.0] - 2023-08-09 -- Added `delete` vault API support. -## [1.9.0] - 2023-06-08 -### Added -- `redaction` key for detokenize method for column group support. - -## [1.8.2] - 2023-03-20 -### Fixed -- removed grace period logic for bearer token generation. - -## [1.8.1] - 2023-03-01 -### Fixed -- java cached token bug - -## [1.8.0] - 2023-01-10 -### Added -- `update` vault API -- `get` vault API - -## [1.7.1] - 2022-11-29 -### Changed -- `setContext` to `setCtx` method. -- `setTimetoLive` accepts seconds in `Integer` instead of `Double`. - -## [1.7.0] - 2022-11-22 -### Added -- `upsert` support for insert method. - -## [1.6.0] - 2022-10-11 - -### Added -- Added Support for Context Aware Authorization. -- Added Support to generate scoped skyflow bearer tokens. -## [1.5.0] - 2022-04-12 - -### Added -- support for application/x-www-form-urlencoded and multipart/form-data content-type's in connections. - -## [1.4.1] - 2022-03-29 - -### Fixed -- Request headers not getting overridden due to case sensitivity - -## [1.4.0] - 2022-03-15 - -### Changed - -- deprecated `isValid` in favour of `isExpired` - -## [1.3.0] - 2022-02-24 - -### Added - -- `requestId` in error logs and error responses for API Errors -- `isValid` method for validating Service Account bearer token - -## [1.2.0] - 2022-01-11 - -### Added -- Logging functionality -- `Configuration.setLogLevel` function for setting the package-level LogLevel -- `generateBearerTokenFromCreds` function which takes credentials as string - -### Changed -- Renamed and deprecated `GenerateToken` in favor of `generateBearerToken` -- `vaultID` and `vaultURL` are optional in `SkyflowConfiguration` constructor - -## [1.1.0] - 2021-11-10 -### Added -- `insert` vault API -- `detokenize` vault API -- `getById` vault API -- `invokeConnection` -## [1.0.1] - 2021-10-20 -### Added -- Service Account Token generation \ No newline at end of file +- Credential JSON field names follow Java camelCase convention: `clientId`, `keyId`, `tokenUri`. Legacy all-caps forms (`clientID`, `keyID`, `tokenURI`) permanently accepted — no migration required. +- Get and Query response maps now return `skyflowId` (camelCase). Legacy `skyflow_id` key retained alongside for backward compatibility; deprecated and will be removed in an upcoming release. +- `GetRequest` and `DetokenizeRequest`: added `downloadUrl()` / `getDownloadUrl()` following acronym-as-word convention. Old `downloadURL()` / `getDownloadURL()` kept as `@Deprecated` delegates. +- `QueryResponse`: added `getErrors()` accessor. +- Removed SDK-level null/empty field value validation from Insert and Update — backend validates per API spec. From 927e1bea3ecd0799f07a6993d2cb3e34cbd9bb89 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 15:59:36 +0530 Subject: [PATCH 32/48] docs: simplify CHANGELOG to point to GitHub and Maven releases Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e3390d..9cf57026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this project will be documented as part of the release notes. -## [2.0.4] - 2025-05-15 -### Changed -- Credential JSON field names follow Java camelCase convention: `clientId`, `keyId`, `tokenUri`. Legacy all-caps forms (`clientID`, `keyID`, `tokenURI`) permanently accepted — no migration required. -- Get and Query response maps now return `skyflowId` (camelCase). Legacy `skyflow_id` key retained alongside for backward compatibility; deprecated and will be removed in an upcoming release. -- `GetRequest` and `DetokenizeRequest`: added `downloadUrl()` / `getDownloadUrl()` following acronym-as-word convention. Old `downloadURL()` / `getDownloadURL()` kept as `@Deprecated` delegates. -- `QueryResponse`: added `getErrors()` accessor. -- Removed SDK-level null/empty field value validation from Insert and Update — backend validates per API spec. +See [GitHub](https://github.com/skyflowapi/skyflow-java/releases) or [Maven](https://mvnrepository.com/artifact/com.skyflow/skyflow-java) for more details on each released version. From 673e57bf989661179d229ca6c986ab4fba140821 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:01:17 +0530 Subject: [PATCH 33/48] docs: add v2 banner to README with migration link and EOL notice Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e97c545d..5f1f0338 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Skyflow Java +> **This is the current, recommended version of the Skyflow Java SDK.** +> V2 (latest: **2.0.4**) 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**. +> +> **Coming soon:** v2.1.x will include public interface nomenclature improvements and additional deprecation signals. See [CHANGELOG](CHANGELOG.md) for details. + 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) From 18821fcae3ddbf6a25993a94056500dd9ad758b9 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:03:27 +0530 Subject: [PATCH 34/48] docs: use release notes link instead of CHANGELOG in banner Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f1f0338..31aa6450 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > > 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**. > -> **Coming soon:** v2.1.x will include public interface nomenclature improvements and additional deprecation signals. See [CHANGELOG](CHANGELOG.md) for details. +> **Coming soon:** v2.1.x will include public interface nomenclature improvements and additional deprecation signals. See [release notes](https://github.com/skyflowapi/skyflow-java/releases) for details. The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend. From 37a1c955cda48350eabc452e336c8a00fc9058af Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:05:16 +0530 Subject: [PATCH 35/48] docs: update README banner to v2.1.x announcement Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 31aa6450..31a9bb7d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # Skyflow Java -> **This is the current, recommended version of the Skyflow Java SDK.** -> V2 (latest: **2.0.4**) 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**. -> -> **Coming soon:** v2.1.x will include public interface nomenclature improvements and additional deprecation signals. See [release notes](https://github.com/skyflowapi/skyflow-java/releases) for details. +> **Java V2.1.x IS NOW AVAILABLE:** A new, improved version of the Skyflow SDK is ready with flexible authentication, multi-vault support, builder patterns, and richer error diagnostics. V1 is in maintenance mode (security patches only) and will reach End of Life on October 31, 2026. We recommend upgrading to v2 — see the **[Migration Guide](docs/migrate_to_v2.md)** for step-by-step instructions. The Skyflow Java SDK is designed to help with integrating Skyflow into a Java backend. From a83a28a998298a81b106d25a30593a8b5372d6bb Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:11:49 +0530 Subject: [PATCH 36/48] =?UTF-8?q?revert:=20remove=20.claude/=20and=20CLAUD?= =?UTF-8?q?E.md=20=E2=80=94=20will=20be=20raised=20as=20separate=20PR=20fr?= =?UTF-8?q?om=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/commands/code-review.md | 135 ------------------------------ .claude/commands/code-security.md | 58 ------------- .claude/commands/sdk-sample.md | 60 ------------- .claude/commands/test.md | 72 ---------------- .claude/settings.json | 56 ------------- CLAUDE.md | 117 -------------------------- 6 files changed, 498 deletions(-) delete mode 100644 .claude/commands/code-review.md delete mode 100644 .claude/commands/code-security.md delete mode 100644 .claude/commands/sdk-sample.md delete mode 100644 .claude/commands/test.md delete mode 100644 .claude/settings.json delete mode 100644 CLAUDE.md diff --git a/.claude/commands/code-review.md b/.claude/commands/code-review.md deleted file mode 100644 index 23653ac2..00000000 --- a/.claude/commands/code-review.md +++ /dev/null @@ -1,135 +0,0 @@ -You are a senior engineer performing a thorough code review on the Skyflow Java SDK. - -## Review Mode - -Use `$ARGUMENTS` to determine scope: -- `full review` — scan all files under `src/main/java/com/skyflow/` recursively (exclude `generated/`) -- A file or directory path — review only that path -- Empty / default — review files changed on current branch vs `main`: - ```bash - git diff main...HEAD --name-only | grep '\.java$' | grep -v 'generated' - ``` - -**Skip entirely:** `src/main/java/com/skyflow/generated/` — Fern-generated REST client, read-only. - ---- - -## 1. Request / Response / Options patterns - -- Request builders are plain data holders — validation happens in `Validations.validateXxxRequest()` inside the controller, not in `build()`. Flag if validation logic is duplicated outside `Validations`. -- Response getters returning `ArrayList>` is the established SDK pattern — do not flag these as violations. -- All response classes must have `getErrors()` returning `null` (not absent) when no errors. -- No separate `*Options` classes exist — options are fields on the request builder itself. -- SDK must not add field-level null/empty validation on top of what the backend enforces. Only structural checks (`table == null`, `values == null`) are permitted. - ---- - -## 2. Error handling - -- All public methods must declare `throws SkyflowException` -- `SkyflowException` must be thrown (not swallowed) on invalid input -- No `System.out.println` or bare `e.printStackTrace()` — use `LogUtil` -- Catch blocks must not silently drop exceptions -- `catch (Exception e)` without re-throw or explicit handling is a critical issue - ---- - -## 3. Naming conventions - -- Classes: `PascalCase` -- Methods / fields: `camelCase` — acronyms as words: `skyflowId` not `skyflowID`, `tokenUri` not `tokenURI`, `downloadUrl` not `downloadURL` -- Constants: `UPPER_SNAKE_CASE` -- Builder setter methods: `setFooId()` not `setFooID()` -- Deprecated methods must use `@Deprecated(since = "x.x", forRemoval = true)` + `@deprecated` Javadoc with `{@link}` to the replacement - ---- - -## 4. Response field normalisation - -- All response maps must use `skyflowId` (camelCase), never `skyflow_id` (snake_case) -- `getErrors()` must be present on every response class - ---- - -## 5. Test coverage - -- Every public method must have at least one positive and one negative test -- Tests must use `Assert.assertEquals` / `Assert.assertNull` — not just `Assert.fail` guards -- No mocking of the production class under test -- Reflection-based tests on private methods are acceptable only when no public API exercises the method - ---- - -## 6. Code quality - -- No magic strings for API field names — use `Constants` or `ErrorMessage` enums -- No duplicate validation logic across request classes — belongs in `Validations` -- No `@SuppressWarnings` without a comment explaining why -- `LogUtil.printWarningLog` must be used for deprecation warnings, not `System.err` - ---- - -## 7. Code smells - -Code smells are structural signals — they may not need immediate fixes but must be flagged. Report them at **Smell** severity. - -### Method & class size -- **Long method** — any method over 40 lines. Candidate for decomposition into private helpers. -- **Long class** — any class over 300 lines. May be taking on too many responsibilities. -- **Large parameter list** — more than 4 parameters on a method. Consider a config/options object. - -### Responsibility violations -- **Business logic in Request/Response classes** — these are data holders. If a Request/Response class contains conditional logic beyond null-safe getters, flag it. -- **toString() with business logic** — `toString()` should only serialise state. Logic like field renaming, manual JSON construction, or conditional field injection belongs in the controller or formatter methods. -- **Validation outside Validations.java** — any `if (x == null) throw new SkyflowException(...)` outside `src/main/java/com/skyflow/utils/validations/` is misplaced. - -### Control flow -- **Deep nesting** — more than 3 levels of `if`/`for`/`try` nesting. Extract inner blocks to named methods. -- **Long if-else chains** — more than 4 branches. Consider a map, switch, or polymorphism. -- **Null checks scattered** — multiple consecutive null guards that could be replaced with `Optional` or early return. - -### Data -- **Magic numbers** — literal integers or sizes (e.g. `25`, `3600`, `100`) without a named constant. Use `Constants`. -- **Raw HashMap chains** — `HashMap` passed through more than 2 method boundaries without a typed wrapper or comment explaining why. Flag for awareness; don't require a fix. -- **Temporary field** — a class field that is only set in certain code paths and `null` the rest of the time. Should be a local variable or method parameter instead. - -### Dead code -- **Unused private methods** — private methods with no callers. -- **Unused imports** — any `import` not referenced in the file. -- **Unreachable code** — code after `return`/`throw` in the same branch. -- **Commented-out code** — blocks of commented code without explanation. Remove or add a TODO with a ticket reference. - -### Comments -- **Explains what, not why** — a comment that restates what the code does (`// get the vault ID`) is noise. Only flag comments that explain the *what* without adding *why*. -- **Stale comment** — a comment that contradicts the current code (e.g. references a removed parameter or old method name). - ---- - -## Output Format - -Group findings by file. For each file: - -``` -### path/to/File.java - -| Severity | Line | Finding | -|------------|------|------------------------------------------------------------| -| Critical | 42 | SkyflowException swallowed in catch block | -| Bug | 87 | skyflow_id not normalised to skyflowId | -| Quality | 103 | Magic string "records" — use Constants | -| Smell | 210 | toString() renames map keys — move to formatter method | -| Smell | 315 | Method is 58 lines — candidate for decomposition | -``` - -**Severities:** -| Level | Meaning | -|---|---| -| **Critical** | Data loss, silent failure, security risk — must fix before merge | -| **Bug** | Wrong behaviour, incorrect output — must fix before merge | -| **Edge Case** | Unhandled input that will cause runtime failure — fix before merge | -| **Quality** | Maintainability issue, naming violation, missing pattern — fix before merge | -| **Smell** | Structural signal, technical debt — flag and track, fix when in the area | - -End with: -1. A tech-debt summary table grouped by category (Error handling / Naming / Smells / Tests) -2. A verdict: `APPROVE` / `APPROVE WITH FIXES` / `REQUEST CHANGES` diff --git a/.claude/commands/code-security.md b/.claude/commands/code-security.md deleted file mode 100644 index 7a2ffcf6..00000000 --- a/.claude/commands/code-security.md +++ /dev/null @@ -1,58 +0,0 @@ -You are a security engineer auditing the Skyflow Java SDK for vulnerabilities. - -## Audit Scope - -Use `$ARGUMENTS` to determine target files. If none provided, run: -```bash -git diff main...HEAD --name-only | grep '\.java$' | grep -v 'generated' -``` - -**Skip:** `src/main/java/com/skyflow/generated/` — observations only, no edits. - -## Security Checks - -### 1. Credential and token exposure (Critical) -- Bearer tokens, API keys, and private keys must never appear in logs, error messages, exception messages, or `toString()` output -- `Credentials` fields (`path`, `token`, `apiKey`, `credentialsString`) must not be serialised to logs -- JWT claims must not be logged - -### 2. Input validation (High) -- All string inputs from callers must be null/empty checked before use -- File paths passed to `new File(path)` must not allow path traversal (`../`) -- JSON strings parsed with `JsonParser` must be wrapped in try/catch for `JsonSyntaxException` - -### 3. Credentials file handling (High) -- Credentials files must only be read from paths provided by the caller — no environment variable path injection without sanitisation -- `FileReader` must be in a try-with-resources or explicitly closed - -### 4. HTTP security (Medium) -- All API calls must go over HTTPS — verify `Utils.getBaseURL` enforces this -- Authorization headers must not be logged at any log level -- HTTP timeouts must be configured - -### 5. Error information leakage (Medium) -- `SkyflowException` messages must not include raw server response bodies that could contain PII -- Stack traces must not be surfaced to callers — wrap in `SkyflowException` - -### 6. Dependency vulnerabilities (Low) -- Note any dependencies that are known to have CVEs (check pom.xml versions) - -### 7. Authentication lifecycle (Medium) -- Bearer token caching must check expiry before reuse -- Token refresh must be thread-safe (`synchronized` or equivalent) - -## Output Format - -For each finding: - -``` -### path/to/File.java : line N - -**Severity:** Critical / High / Medium / Low / Info -**Risk:** What an attacker could do -**Trigger:** Input or code path that triggers the vulnerability -**Fix:** Concrete remediation with code example -**CWE:** CWE-NNN -``` - -End with a summary table and overall risk rating. diff --git a/.claude/commands/sdk-sample.md b/.claude/commands/sdk-sample.md deleted file mode 100644 index 79e702aa..00000000 --- a/.claude/commands/sdk-sample.md +++ /dev/null @@ -1,60 +0,0 @@ -Create a Skyflow Java SDK sample file demonstrating: $ARGUMENTS - -## File placement - -| Feature type | Package | Directory | -|---|---|---| -| Vault ops (insert/get/update/delete/query/tokenize) | `com.example.vault` | `samples/src/main/java/com/example/vault/` | -| Service account auth | `com.example.serviceaccount` | `samples/src/main/java/com/example/serviceaccount/` | -| Connection | `com.example.connection` | `samples/src/main/java/com/example/connection/` | -| Detect | `com.example.detect` | `samples/src/main/java/com/example/detect/` | - -File name: `Example.java` - -## Structure (follow this order) - -1. Package declaration -2. Imports — only from `com.skyflow.*`, `java.*`; never from `com.skyflow.generated.*` -3. Public class with `main(String[] args) throws SkyflowException` -4. Credentials setup — choose based on feature: - - **Vault ops:** `credentials.setApiKey("")` or `credentials.setCredentialsString("")` - - **Service account:** `credentials.setPath("credentials.json")` (path to the service account JSON file) -5. `VaultConfig` with `setVaultId`, `setClusterId`, `setEnv(Env.PROD)`, `setCredentials(credentials)` -6. Build the Skyflow client: - ```java - Skyflow skyflowClient = Skyflow.builder() - .setLogLevel(LogLevel.DEBUG) - .addVaultConfig(vaultConfig) - .build(); - ``` -7. Request object via `*Request.builder()` — options go directly on the builder (no separate Options class): - ```java - // Example: InsertRequest with tokenMode - InsertRequest request = InsertRequest.builder() - .table("...") - .values(records) - .tokenMode(TokenMode.ENABLE) - .build(); - ``` -8. Call the vault method inside a try/catch for `SkyflowException`: - ```java - InsertResponse response = skyflowClient.vault().insert(request); - System.out.println(response); - ``` - -## Rules - -- Vault IDs / cluster IDs use placeholders: `""`, `""` -- Credential values use placeholders: `""`, `""` -- Credentials file path: `"credentials.json"` (relative — no absolute paths) -- Always catch `SkyflowException` and print `e.getMessage()` -- No separate `*Options` classes — they don't exist in this SDK; use request builder methods -- Keep under 80 lines - -## After creating the file - -```bash -cd samples && mvn compile -q 2>&1 | tail -20 -``` - -Report the file path and any compile errors. diff --git a/.claude/commands/test.md b/.claude/commands/test.md deleted file mode 100644 index 98397f8f..00000000 --- a/.claude/commands/test.md +++ /dev/null @@ -1,72 +0,0 @@ -Run the Skyflow Java SDK quality pipeline. - -Use `$ARGUMENTS` to target a specific test class (e.g. `BearerTokenTests`). If empty, run the full suite. - -## Known Pre-existing Failures (not regressions) - -Before reporting failures, check against this baseline: -- `HttpUtilityTests` — ALL tests fail (JDK 21 + PowerMock `InaccessibleObject` incompatibility) -- `TokenTests#testExpiredTokenForIsExpiredToken` — needs live credentials -- `VaultClientTests#testSetBearerTokenWithEnvCredentials` — needs `SKYFLOW_CREDENTIALS` env var -- `ConnectionClientTests#testSetBearerTokenWithEnvCredentials` — needs `SKYFLOW_CREDENTIALS` env var - -Baseline: 374 tests, ~5 failures, ~4 errors. Only report failures **beyond** this baseline. - -## Pipeline - -### Step 1 — Compile -```bash -mvn compile -q 2>&1 | tail -20 -``` -Expected: no output (clean compile). Report any errors. - -### Step 2 — Checkstyle -```bash -mvn checkstyle:check -q 2>&1 | tail -20 -``` -Note: `failsOnError=false` in pom.xml means the build will not fail even if violations exist — check the output for `[WARN]` checkstyle lines. Violations are excluded from `generated/` by pom config. - -### Step 3 — Build -```bash -mvn package -DskipTests -q 2>&1 | tail -20 -``` -Expected: BUILD SUCCESS. - -### Step 4 — Tests -If `$ARGUMENTS` is set: -```bash -mvn test -Dtest=$ARGUMENTS -q 2>&1 | tail -40 -``` -Otherwise: -```bash -mvn test -q 2>&1 | tail -40 -``` -Report: tests run, failures, errors. Flag any pre-existing failures separately from new ones. - -### Step 5 — Coverage analysis -Flag any public interface class (`src/main/java/com/skyflow/vault/`, `src/main/java/com/skyflow/config/`, `src/main/java/com/skyflow/serviceaccount/`) that has no corresponding test file under `src/test/`. - -For classes that do have tests, check whether each public method has at least one positive and one negative test case. List any gaps. - -### Step 6 — Edge case identification -For any test class below complete coverage, identify missing scenarios: -- Null / empty inputs -- Invalid types / wrong enum values -- Concurrent / reuse scenarios -- Error paths (API rejection, network failure) - -Write concrete JUnit 4 test method stubs (not full implementations) for each gap. - -### Step 7 — Report - -``` -| Step | Status | Notes | -|---|---|---| -| Compile | ✅ / ❌ | ... | -| Checkstyle | ✅ / ❌ | ... | -| Build | ✅ / ❌ | ... | -| Tests | ✅ / ❌ | N passed, M failed | -| Coverage gaps | ... | list classes | -``` - -Conclude with **READY TO MERGE** or **NEEDS FIXES** and a prioritised fix list. diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 3c084d46..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n r = subprocess.run(['mvn', 'checkstyle:check', '-q'], capture_output=True, text=True)\n out = (r.stdout + r.stderr).strip()\n if out:\n lines = out.splitlines()\n print('\\n'.join(lines[-20:]))\n\"" - } - ] - }, - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "python3 -c \"\nimport sys, json, subprocess\nd = json.load(sys.stdin)\nf = d.get('tool_input', {}).get('file_path', d.get('file_path', ''))\nif f and f.endswith('.java') and 'generated' not in f:\n r = subprocess.run(['mvn', 'compile', '-q'], capture_output=True, text=True)\n out = (r.stdout + r.stderr).strip()\n if out:\n lines = out.splitlines()\n print('\\n'.join(lines[-20:]))\n\"" - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',d.get('file_path','')); banned='generated'; (sys.stderr.write('BLOCKED: Fern-generated code — do not edit manually\\n'), sys.exit(2)) if banned in p and 'src/main/java/com/skyflow/generated' in p else sys.exit(0)\"" - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || true" - } - ] - } - ] - }, - "permissions": { - "allow": [ - "Bash(mvn *)", - "Bash(java *)", - "Bash(python3 *)" - ], - "deny": [ - "Edit(src/main/java/com/skyflow/generated/**)", - "Write(src/main/java/com/skyflow/generated/**)" - ] - } -} diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1e1d966b..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -name: skyflow-java-sdk -description: Skyflow Java SDK project context — naming conventions, build commands, known failures, and slash commands. Loaded for all Java source, test, and sample files. -paths: - - src/**/*.java - - samples/**/*.java - - pom.xml - - checkstyle.xml ---- - -# Skyflow Java SDK — Claude Code Instructions - -## Project Overview - -This is the Skyflow Java SDK (`skyflow-java`). It provides a Java interface to the Skyflow Data Privacy Vault API — vault operations (insert, get, update, delete, query, tokenize, detokenize), service account authentication (bearer tokens, signed data tokens), connections, detect, and audit. - -**Current version:** 2.x (v3.0.0 in preparation — see `docs/superpowers/specs/`) - -## Critical Boundary — Generated Code - -**Never edit files under `src/main/java/com/skyflow/generated/`.** - -These are auto-generated by [Fern](https://buildwithfern.com) from the Skyflow API definition. Manual edits are overwritten on the next generation run. If you find a bug in generated code, report it — do not patch it directly. - -The `pom.xml` checkstyle and test configs already exclude `generated/` from all checks. - -## Project Structure - -``` -src/ - main/java/com/skyflow/ - config/ # VaultConfig, Credentials, ConnectionConfig - vault/ - controller/ # VaultController, AuditController, BinLookupController, - # ConnectionController, DetectController - data/ # Request/Response objects: InsertRequest, GetResponse, etc. - tokens/ # DetokenizeRequest/Response, TokenizeRequest/Response - connection/ # InvokeConnectionRequest/Response - audit/ # ListEventRequest/Response - bin/ # GetBinRequest/Response (BIN lookup) - detect/ # Deidentify/Reidentify requests/responses - serviceaccount/ - util/ # BearerToken, SignedDataTokens — credential parsing + JWT - enums/ # LogLevel, RedactionType, TokenMode, Env, etc. - errors/ # SkyflowException, ErrorCode, ErrorMessage - utils/ # Utils, Constants, HttpUtility, LogUtil - generated/ # ← FERN-GENERATED, DO NOT EDIT - test/java/com/skyflow/ - ... # JUnit 4 tests mirroring the main structure -samples/ # Standalone Maven project — com.example.vault / .serviceaccount / .detect / .connection -docs/ - superpowers/ - specs/ # Design specs for in-progress features - plans/ # Implementation plans -``` - -## Naming Conventions - -- **Acronyms as words:** `skyflowId` (not `skyflowID`), `clientId` (not `clientID`), `tokenUri` (not `tokenURI`), `keyId` (not `keyID`) -- **Builder setters:** `setVaultId()`, `setClusterId()`, `setSkyflowId()` — never `setVaultID()` -- **Response maps:** always use `skyflowId` (camelCase) — the raw API returns `skyflow_id` (snake_case) which VaultController normalises before returning to callers -- **Constants class:** use `com.skyflow.utils.Constants` for string literals; `ErrorMessage` enum for error message strings - -## Build and Test - -```bash -mvn compile -q # compile -mvn checkstyle:check -q # lint (config: checkstyle.xml) -mvn test -q # full test suite (JUnit 4) -mvn test -Dtest=ClassName # single test class -mvn package -DskipTests -q # build jar -``` - -Samples (separate Maven project): -```bash -cd samples && mvn compile -q -``` - -## Credentials JSON Format - -The SDK reads a `credentials.json` file for service account authentication. The canonical field names (v3+) are: - -```json -{ - "clientId": "...", - "keyId": "...", - "tokenUri": "...", - "privateKey": "..." -} -``` - -The legacy all-caps forms (`clientID`, `keyID`, `tokenURI`) are accepted as fallbacks for migration. - -## Known Pre-existing Test Failures - -These failures exist on `main` and are **not regressions** — do not investigate them unless specifically asked: - -| Test class | Failure | Cause | -|---|---|---| -| `HttpUtilityTests` | `InaccessibleObject` (all tests) | JDK 21 + PowerMock incompatibility — PowerMock cannot reflect into `java.net` | -| `TokenTests#testExpiredTokenForIsExpiredToken` | Environment error | Requires live credentials | -| `VaultClientTests#testSetBearerTokenWithEnvCredentials` | Environment error | Requires `SKYFLOW_CREDENTIALS` env var | -| `ConnectionClientTests#testSetBearerTokenWithEnvCredentials` | Environment error | Requires `SKYFLOW_CREDENTIALS` env var | - -Run `mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR"` to see current baseline (374 tests, ~5 failures, ~4 errors). - -## Active Work - -See `docs/superpowers/specs/` for in-progress design specs and `docs/superpowers/plans/` for implementation plans. - -## Slash Commands - -- `/code-review` — full review: SDK patterns + code smells + security checks (reads `.claude/commands/code-smell.md` and `.claude/commands/code-security.md` inline) -- `/code-smell` — standalone structural smell analysis only (long methods, dead code, misplaced logic) -- `/code-security` — standalone security audit only (credentials, input validation, HTTP security) -- `/sdk-sample ` — generate a sample file for a feature -- `/test [ClassName]` — run quality pipeline (compile → checkstyle → build → test → coverage) From 06e55a14862f19a3e17646a003399cbe5a379a56 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:31:41 +0530 Subject: [PATCH 37/48] chore: remove superpowers planning docs from repo Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../2026-05-13-java-nomenclature-cleanup.md | 589 --------------- ...4-v2-backward-compatibility-deprecation.md | 673 ------------------ ...-05-13-java-nomenclature-cleanup-design.md | 133 ---- 3 files changed, 1395 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md delete mode 100644 docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md delete mode 100644 docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md diff --git a/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md b/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md deleted file mode 100644 index a386accd..00000000 --- a/docs/superpowers/plans/2026-05-13-java-nomenclature-cleanup.md +++ /dev/null @@ -1,589 +0,0 @@ -# Java SDK Nomenclature Cleanup Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Rename credential JSON field keys (`clientID`→`clientId`, `keyID`→`keyId`, `tokenURI`→`tokenUri`) with fallback support, normalise `skyflow_id`→`skyflowId` in Get and Query responses, and add `getErrors()` to `QueryResponse`. - -**Architecture:** Three independent, targeted changes to existing files — no new files, no new abstractions. Each change is a surgical edit to one method or class, verified by unit tests that already exist or that we add inline. - -**Tech Stack:** Java 11+, JUnit 4, Mockito/PowerMock, Maven (`mvn test`) - -**Design spec:** `docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md` - ---- - -## File Map - -| File | Change | -|---|---| -| `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java` | Fallback lookup for `clientId`/`keyId`/`tokenUri` in `getBearerTokenFromCredentials` | -| `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java` | Fallback lookup for `clientId`/`keyId` in `generateSignedTokensFromCredentials` | -| `src/main/java/com/skyflow/vault/controller/VaultController.java` | Rename `skyflow_id`→`skyflowId` in `getFormattedGetRecord` and `getFormattedQueryRecord` | -| `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `errors` field and `getErrors()` accessor | -| `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` | Add tests for new-form keys, old-form fallback, and missing-key errors | -| `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` | Add tests for new-form keys, old-form fallback, and missing-key errors | -| `src/test/java/com/skyflow/vault/data/QueryResponseTest.java` | New file — tests for `getErrors()` always returning null | - ---- - -## Task 1: Credential field renames in BearerToken — new key form - -**Files:** -- Modify: `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java:92-145` -- Modify: `src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java` - -### Background -`getBearerTokenFromCredentials` parses a `JsonObject` representing the credentials file. It currently looks up `clientID`, `keyID`, and `tokenURI`. We need it to accept `clientId`, `keyId`, `tokenUri` (new canonical form) while still accepting the old keys as a fallback. - -The test at line 228 of `BearerTokenTests.java` currently uses the old keys — we need a parallel test using the new keys. - -- [ ] **Step 1: Write a failing test for new-form credential keys** - -Add this test to `BearerTokenTests.java`. It uses a credentials string with `clientId`, `keyId`, `tokenUri` (new form) and expects a `SkyflowException` with the `InvalidTokenUri` message (because the URI value is invalid — not because the keys are unrecognised). This confirms the new keys are read successfully. - -```java -@Test -public void testBearerTokenWithNewFormCredentialKeys() { - try { - String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " - + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}"; - BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build(); - bearerToken.getBearerToken(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); - } -} -``` - -- [ ] **Step 2: Run the test to confirm it fails** - -```bash -mvn test -pl . -Dtest=BearerTokenTests#testBearerTokenWithNewFormCredentialKeys -q -``` - -Expected: FAIL — the test throws `MissingClientId` (because `clientId` is not found, only `clientID` is looked up). - -- [ ] **Step 3: Update `getBearerTokenFromCredentials` in `BearerToken.java`** - -Replace the three field lookups (lines 102–118) with fallback logic: - -```java -JsonElement clientId = credentials.get("clientId"); -if (clientId == null) clientId = credentials.get("clientID"); -if (clientId == null) { - LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); -} - -JsonElement keyId = credentials.get("keyId"); -if (keyId == null) keyId = credentials.get("keyID"); -if (keyId == null) { - LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); -} - -JsonElement tokenUri = credentials.get("tokenUri"); -if (tokenUri == null) tokenUri = credentials.get("tokenURI"); -if (tokenUri == null) { - LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); -} -``` - -Also update the `getSignedToken` call on line 121 to use the renamed variables: - -```java -String signedUserJWT = getSignedToken( - clientId.getAsString(), keyId.getAsString(), tokenUri.getAsString(), pvtKey, context -); -String basePath = Utils.getBaseURL(tokenUri.getAsString()); -``` - -Also update the private method signature at line 147–148 to use idiomatic parameter names (internal only, no public impact): - -```java -private static String getSignedToken( - String clientId, String keyId, String tokenUri, PrivateKey pvtKey, Object context -) { - final Date createdDate = new Date(); - final Date expirationDate = new Date(createdDate.getTime() + (3600 * 1000)); - io.jsonwebtoken.JwtBuilder builder = Jwts.builder() - .claim("iss", clientId) - .claim("key", keyId) - .claim("aud", tokenUri) - .claim("sub", clientId) - .expiration(expirationDate); - if (context != null) { - builder.claim("ctx", context); - } - return builder.signWith(pvtKey, Jwts.SIG.RS256).compact(); -} -``` - -- [ ] **Step 4: Run the new test to confirm it passes** - -```bash -mvn test -pl . -Dtest=BearerTokenTests#testBearerTokenWithNewFormCredentialKeys -q -``` - -Expected: PASS — `clientId` is found, execution reaches `InvalidTokenUri`. - -- [ ] **Step 5: Run the full BearerToken test suite to confirm no regressions** - -```bash -mvn test -pl . -Dtest=BearerTokenTests -q -``` - -Expected: All existing tests pass (old-form keys `clientID`/`keyID`/`tokenURI` still work via fallback). - -- [ ] **Step 6: Commit** - -```bash -git add src/main/java/com/skyflow/serviceaccount/util/BearerToken.java \ - src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java -git commit -m "feat: accept clientId/keyId/tokenUri in BearerToken with fallback to old form" -``` - ---- - -## Task 2: Credential field renames in SignedDataTokens — new key form - -**Files:** -- Modify: `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java:92-122` -- Modify: `src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java` - -### Background -`generateSignedTokensFromCredentials` parses a credentials `JsonObject` and looks up `clientID` and `keyID`. Same rename as Task 1, but no `tokenURI` (SignedDataTokens does not need it). - -- [ ] **Step 1: Check what the existing SignedDataTokens test uses for credential keys** - -```bash -grep -n "clientID\|keyID\|clientId\|keyId" src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java -``` - -Note the line number of any credentials string that uses `clientID`/`keyID` — you will add a parallel test using the new keys. - -- [ ] **Step 2: Write a failing test for new-form keys** - -Add this test to `SignedDataTokensTests.java`. It expects the token generation to fail at the private key parsing stage (not at the field-lookup stage), confirming `clientId` and `keyId` are successfully read: - -```java -@Test -public void testSignedDataTokensWithNewFormCredentialKeys() { - try { - String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " - + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\"}"; - ArrayList dataTokens = new ArrayList<>(); - dataTokens.add("test-token"); - SignedDataTokens signedDataTokens = SignedDataTokens.builder() - .setCredentials(credentialsString) - .setDataTokens(dataTokens) - .build(); - signedDataTokens.getSignedDataTokens(); - Assert.fail(EXCEPTION_NOT_THROWN); - } catch (SkyflowException e) { - // Should fail past field lookup — at private key parsing, not at MissingClientId - Assert.assertNotEquals(ErrorMessage.MissingClientId.getMessage(), e.getMessage()); - Assert.assertNotEquals(ErrorMessage.MissingKeyId.getMessage(), e.getMessage()); - } -} -``` - -- [ ] **Step 3: Run the test to confirm it fails** - -```bash -mvn test -pl . -Dtest=SignedDataTokensTests#testSignedDataTokensWithNewFormCredentialKeys -q -``` - -Expected: FAIL — throws `MissingClientId` because `clientId` is not yet recognised. - -- [ ] **Step 4: Update `generateSignedTokensFromCredentials` in `SignedDataTokens.java`** - -Replace the `clientID` and `keyID` lookups (lines 103–113) with fallback logic: - -```java -JsonElement clientId = credentials.get("clientId"); -if (clientId == null) clientId = credentials.get("clientID"); -if (clientId == null) { - LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); -} - -JsonElement keyId = credentials.get("keyId"); -if (keyId == null) keyId = credentials.get("keyID"); -if (keyId == null) { - LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); -} -``` - -Update the `getSignedToken` call on line 115 to use the renamed variables: - -```java -signedDataTokens = getSignedToken( - clientId.getAsString(), keyId.getAsString(), pvtKey, dataTokens, timeToLive, context); -``` - -Update the private method signature at line 124–125 to use idiomatic parameter names: - -```java -private static List getSignedToken( - String clientId, String keyId, PrivateKey pvtKey, - ArrayList dataTokens, Integer timeToLive, Object context -) { -``` - -And update the JWT claims inside `getSignedToken` (lines 142–143): - -```java -.claim("key", keyId) -.claim("sub", clientId) -``` - -- [ ] **Step 5: Run the new test to confirm it passes** - -```bash -mvn test -pl . -Dtest=SignedDataTokensTests#testSignedDataTokensWithNewFormCredentialKeys -q -``` - -Expected: PASS — `clientId` and `keyId` are found; exception is from private key parsing, not from missing fields. - -- [ ] **Step 6: Run the full SignedDataTokens test suite** - -```bash -mvn test -pl . -Dtest=SignedDataTokensTests -q -``` - -Expected: All existing tests pass. - -- [ ] **Step 7: Commit** - -```bash -git add src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java \ - src/test/java/com/skyflow/serviceaccount/util/SignedDataTokensTests.java -git commit -m "feat: accept clientId/keyId in SignedDataTokens with fallback to old form" -``` - ---- - -## Task 3: Normalise `skyflow_id` → `skyflowId` in Get and Query responses - -**Files:** -- Modify: `src/main/java/com/skyflow/vault/controller/VaultController.java:121-152` -- Modify: `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` - -### Background -`getFormattedGetRecord` and `getFormattedQueryRecord` call `putAll(fieldsOpt.get())` which passes through the raw API map — including the `skyflow_id` snake_case key from the wire format. Insert and Update responses already use `skyflowId`. This inconsistency means callers must know which operation produced the response in order to read the record ID. - -The test suite does not currently test the contents of Get or Query responses (no existing tests for `skyflowId` in these paths), so we add new unit tests. - -Because the actual vault API is not called in unit tests (no mock infrastructure for it in `VaultControllerTests`), we test the formatter methods indirectly by verifying the behaviour of the public `get()` and `query()` methods throw the right validation errors — and we test the formatters directly via reflection, or we add a thin package-private helper. - -The simplest approach: add package-private unit tests for the two static formatter methods directly. - -- [ ] **Step 1: Write failing tests for the formatter methods** - -Add these tests to `VaultControllerTests.java`: - -```java -import com.skyflow.generated.rest.types.V1FieldRecords; -import java.util.HashMap; -import java.util.Map; -import java.lang.reflect.Method; - -@Test -public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { - Map fields = new HashMap<>(); - fields.put("skyflow_id", "abc-123"); - fields.put("name", "John"); - V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); - - Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); - method.setAccessible(true); - @SuppressWarnings("unchecked") - HashMap result = (HashMap) method.invoke(null, record); - - Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); - Assert.assertEquals("skyflowId should be present", "abc-123", result.get("skyflowId")); - Assert.assertEquals("other fields should be preserved", "John", result.get("name")); -} - -@Test -public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { - Map fields = new HashMap<>(); - fields.put("skyflow_id", "xyz-456"); - fields.put("email", "test@example.com"); - V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); - - Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); - method.setAccessible(true); - @SuppressWarnings("unchecked") - HashMap result = (HashMap) method.invoke(null, record); - - Assert.assertFalse("skyflow_id (snake_case) should not be present", result.containsKey("skyflow_id")); - Assert.assertEquals("skyflowId should be present", "xyz-456", result.get("skyflowId")); - Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); -} -``` - -- [ ] **Step 2: Run the tests to confirm they fail** - -```bash -mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId -q -``` - -Expected: FAIL — `skyflow_id` is present in the result, `skyflowId` is absent. - -- [ ] **Step 3: Update `getFormattedGetRecord` in `VaultController.java`** - -After the `putAll` block (after line 131), add the key rename: - -```java -private static synchronized HashMap getFormattedGetRecord(V1FieldRecords record) { - HashMap getRecord = new HashMap<>(); - - Optional> fieldsOpt = record.getFields(); - Optional> tokensOpt = record.getTokens(); - - if (fieldsOpt.isPresent()) { - getRecord.putAll(fieldsOpt.get()); - } else if (tokensOpt.isPresent()) { - getRecord.putAll(tokensOpt.get()); - } - - if (getRecord.containsKey("skyflow_id")) { - getRecord.put("skyflowId", getRecord.remove("skyflow_id")); - } - - return getRecord; -} -``` - -- [ ] **Step 4: Update `getFormattedQueryRecord` in `VaultController.java`** - -After the `putAll` block (after line 150), add the key rename: - -```java -private static synchronized HashMap getFormattedQueryRecord(V1FieldRecords record) { - HashMap queryRecord = new HashMap<>(); - Optional> fieldsOpt = record.getFields(); - if (fieldsOpt.isPresent()) { - queryRecord.putAll(fieldsOpt.get()); - } - - if (queryRecord.containsKey("skyflow_id")) { - queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); - } - - return queryRecord; -} -``` - -- [ ] **Step 5: Run the new tests to confirm they pass** - -```bash -mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId -q -``` - -Expected: PASS. - -- [ ] **Step 6: Run the full VaultController test suite** - -```bash -mvn test -pl . -Dtest=VaultControllerTests -q -``` - -Expected: All existing tests pass. - -- [ ] **Step 7: Commit** - -```bash -git add src/main/java/com/skyflow/vault/controller/VaultController.java \ - src/test/java/com/skyflow/vault/controller/VaultControllerTests.java -git commit -m "feat: normalise skyflow_id to skyflowId in Get and Query response maps" -``` - ---- - -## Task 4: Add `getErrors()` to `QueryResponse` - -**Files:** -- Modify: `src/main/java/com/skyflow/vault/data/QueryResponse.java` -- Create: `src/test/java/com/skyflow/vault/data/QueryResponseTest.java` - -### Background -`QueryResponse` is the only response class without a `getErrors()` method. The field is referenced in `toString()` as a hardcoded `null` literal but is not accessible programmatically. We add the field and accessor to match the pattern in `GetResponse`, `InsertResponse`, and `UpdateResponse` (all return `null` when no errors). - -- [ ] **Step 1: Write a failing test** - -Create `src/test/java/com/skyflow/vault/data/QueryResponseTest.java`: - -```java -package com.skyflow.vault.data; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.HashMap; - -public class QueryResponseTest { - - @Test - public void testGetErrorsReturnsNull() { - ArrayList> fields = new ArrayList<>(); - HashMap record = new HashMap<>(); - record.put("skyflowId", "abc-123"); - fields.add(record); - - QueryResponse response = new QueryResponse(fields); - - Assert.assertNull("getErrors() should return null when no errors", response.getErrors()); - } - - @Test - public void testGetErrorsIsPresentInToString() { - QueryResponse response = new QueryResponse(new ArrayList<>()); - String json = response.toString(); - Assert.assertTrue("toString() should include errors field", json.contains("\"errors\"")); - } -} -``` - -- [ ] **Step 2: Run the tests to confirm they fail** - -```bash -mvn test -pl . -Dtest=QueryResponseTest -q -``` - -Expected: FAIL — compile error: `getErrors()` method does not exist on `QueryResponse`. - -- [ ] **Step 3: Update `QueryResponse.java`** - -Add the `errors` field and accessor. The `toString()` no longer needs to manually inject `errors` since `serializeNulls` will include it automatically: - -```java -package com.skyflow.vault.data; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import java.util.ArrayList; -import java.util.HashMap; - -public class QueryResponse { - private final ArrayList> fields; - private final ArrayList> errors; - - public QueryResponse(ArrayList> fields) { - this.fields = fields; - this.errors = null; - } - - public ArrayList> getFields() { - return fields; - } - - public ArrayList> getErrors() { - return errors; - } - - @Override - public String toString() { - Gson gson = new GsonBuilder().serializeNulls().create(); - JsonObject responseObject = gson.toJsonTree(this).getAsJsonObject(); - // tokenizedData is intentionally not surfaced — Query API cannot return tokens - JsonArray fieldsArray = responseObject.get("fields").getAsJsonArray(); - for (JsonElement fieldElement : fieldsArray) { - fieldElement.getAsJsonObject().add("tokenizedData", new JsonObject()); - } - return responseObject.toString(); - } -} -``` - -- [ ] **Step 4: Run the new tests to confirm they pass** - -```bash -mvn test -pl . -Dtest=QueryResponseTest -q -``` - -Expected: PASS. - -- [ ] **Step 5: Run the full test suite to confirm no regressions** - -```bash -mvn test -q -``` - -Expected: All tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/main/java/com/skyflow/vault/data/QueryResponse.java \ - src/test/java/com/skyflow/vault/data/QueryResponseTest.java -git commit -m "feat: add getErrors() accessor to QueryResponse" -``` - ---- - -## Task 5: LOW audit — verify no `setFooID` / `getFooID` violations - -**Files:** -- Read-only audit (no changes expected) - -### Background -The spec requires confirming that all builder setter/getter methods use `setFooId()` / `getFooId()` (title-case `Id`), not `setFooID()`. Initial review of `VaultConfig` already shows `setVaultId()` and `setClusterId()` are correct. This task confirms nothing was missed. - -- [ ] **Step 1: Run the grep audit** - -```bash -grep -rn "set[A-Za-z]*ID\b\|get[A-Za-z]*ID\b" \ - src/main/java/com/skyflow/config/ \ - src/main/java/com/skyflow/vault/data/ \ - src/main/java/com/skyflow/serviceaccount/ \ - --include="*.java" -``` - -Expected output: **no results** — all methods already use title-case `Id`. - -- [ ] **Step 2: If violations are found, rename them** - -For each violation (e.g. `setVaultID` → `setVaultId`), use your editor's rename refactor across all callers, then run: - -```bash -mvn test -q -``` - -Expected: All tests pass. - -- [ ] **Step 3: Commit (only if changes were made)** - -```bash -git add -p -git commit -m "fix: rename setFooID/getFooID to setFooId/getFooId per Java convention" -``` - -If no violations were found, record the result: - -```bash -git commit --allow-empty -m "chore: audit confirms no setFooID/getFooID violations in public API" -``` - ---- - -## Final verification - -- [ ] **Run the complete test suite one last time** - -```bash -mvn test -q -``` - -Expected: All tests pass with no failures or errors. diff --git a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md b/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md deleted file mode 100644 index 51b0562c..00000000 --- a/docs/superpowers/plans/2026-05-14-v2-backward-compatibility-deprecation.md +++ /dev/null @@ -1,673 +0,0 @@ -# V2 Backward Compatibility — Deprecation Warnings Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Restore backward compatibility for v2 customers by keeping old public interface forms alongside new ones, and emit deprecation signals when the old forms are used. - -**Architecture:** Three independent changes. (1) The `skyflow_id` response key was removed — it must be restored alongside `skyflowId` so both exist in the map simultaneously; a WARN log is emitted per response build when the old key is present. (2) The credential field fallback (`clientID`/`keyID`/`tokenURI`) already works silently — add WARN logs when the old form triggers the fallback path. (3) `downloadURL` method names on `GetRequest` and `DetokenizeRequest` violate the acronym-as-word rule — keep the old `@Deprecated` methods alongside new `downloadUrl` methods. All runtime deprecation messages use `LogUtil.printWarningLog()`; method-level deprecation uses the standard Java `@Deprecated` annotation. - -**Tech Stack:** Java 11+, JUnit 4, Maven (`mvn test`) - -**Design context:** `docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md` - ---- - -## Breaking-Change Summary - -| Change | Status | Fix needed | -|---|---|---| -| `skyflow_id` removed from Get/Query response maps | **BREAKING** | Keep both `skyflow_id` + `skyflowId`; emit WARN | -| `clientID`/`keyID`/`tokenURI` in BearerToken | Not breaking — both forms supported permanently | No action needed | -| `clientID`/`keyID` in SignedDataTokens | Not breaking — both forms supported permanently | No action needed | -| `getErrors()` added to QueryResponse | Not breaking (additive) | No change needed | -| `downloadURL` → `downloadUrl` in GetRequest & DetokenizeRequest | **BREAKING** | Keep `@Deprecated` old methods; add new `downloadUrl` methods | - ---- - -## File Map - -| File | Change | -|---|---| -| `src/main/java/com/skyflow/logs/InfoLogs.java` | Add 2 deprecation warning log entries (`skyflow_id` key + `downloadURL` method) | -| `src/main/java/com/skyflow/vault/data/GetRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` + WARN log | -| `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` | Add `getDownloadUrl()` + builder `downloadUrl()`; mark old `getDownloadURL()` as `@Deprecated` + WARN log | -| `src/main/java/com/skyflow/vault/controller/VaultController.java` | Keep `skyflow_id` key alongside `skyflowId`; emit WARN per record | -| `src/main/java/com/skyflow/vault/data/GetResponse.java` | Add `@deprecated` Javadoc on `getData()` for `skyflow_id` key | -| `src/main/java/com/skyflow/vault/data/QueryResponse.java` | Add `@deprecated` Javadoc on `getFields()` for `skyflow_id` key | -| `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` | Update existing tests: assert BOTH `skyflow_id` and `skyflowId` present | - ---- - -## Task 1: Add deprecation log entries to InfoLogs - -**Files:** -- Modify: `src/main/java/com/skyflow/logs/InfoLogs.java` - -### Background -`InfoLogs` is an enum that holds all INFO-level log message strings. Deprecation warnings use `LogUtil.printWarningLog(String)` which takes a plain string — but following the codebase convention of centralising messages in enums, we add the deprecation messages here. They will be passed as `InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()` etc. - -- [ ] **Step 1: Add deprecation entries to InfoLogs enum** - -Open `src/main/java/com/skyflow/logs/InfoLogs.java`. Find the last enum entry before the blank line and constructor (around line 98). Add these four entries in a new section: - -```java - // Deprecation warnings — v2 backward compat, to be removed in v3 - DEPRECATED_SKYFLOW_ID_KEY("[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' 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."); -``` - -Note: The last existing entry before your addition ends with a comma. Your last entry (`DEPRECATED_CREDENTIAL_TOKEN_URI`) ends with a semicolon `;` to close the enum constant list — verify you are replacing the existing semicolon, not duplicating it. - -- [ ] **Step 2: Verify compilation** - -```bash -mvn compile -q 2>&1 | tail -10 -``` - -Expected: no output (clean compile). - -- [ ] **Step 3: Commit** - -```bash -git add src/main/java/com/skyflow/logs/InfoLogs.java -git commit -m "chore: add deprecation warning log entries to InfoLogs" -``` - ---- - -## Task 2: Restore `skyflow_id` key in Get/Query response maps - -**Files:** -- Modify: `src/main/java/com/skyflow/vault/controller/VaultController.java:121-165` -- Modify: `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` - -### Background -`getFormattedGetRecord` and `getFormattedQueryRecord` currently do: -```java -if (getRecord.containsKey("skyflow_id")) { - getRecord.put("skyflowId", getRecord.remove("skyflow_id")); // BREAKS customers using skyflow_id -} -``` -The `remove` deletes `skyflow_id` from the map. We must change this to a **copy** (not a move): put `skyflowId` AND keep `skyflow_id`. Emit one WARN log per record that contains the old key. - -The existing tests `testGetFormattedGetRecordNormalisesSkyflowId` and `testGetFormattedQueryRecordNormalisesSkyflowId` assert `skyflow_id` is **absent** — those assertions must be flipped. - -- [ ] **Step 1: Update the existing tests to assert BOTH keys present** - -In `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java`, find `testGetFormattedGetRecordNormalisesSkyflowId` and `testGetFormattedQueryRecordNormalisesSkyflowId` and update their assertions: - -```java -@Test -public void testGetFormattedGetRecordNormalisesSkyflowId() throws Exception { - Map fields = new HashMap<>(); - fields.put("skyflow_id", "abc-123"); - fields.put("name", "John"); - V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); - - Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); - method.setAccessible(true); - @SuppressWarnings("unchecked") - HashMap result = (HashMap) method.invoke(null, record); - - // Both keys must be present — skyflow_id kept for v2 backward compat, skyflowId is the new form - Assert.assertEquals("skyflowId should be present (new form)", "abc-123", result.get("skyflowId")); - Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "abc-123", result.get("skyflow_id")); - Assert.assertEquals("other fields should be preserved", "John", result.get("name")); -} - -@Test -public void testGetFormattedQueryRecordNormalisesSkyflowId() throws Exception { - Map fields = new HashMap<>(); - fields.put("skyflow_id", "xyz-456"); - fields.put("email", "test@example.com"); - V1FieldRecords record = V1FieldRecords.builder().fields(fields).build(); - - Method method = VaultController.class.getDeclaredMethod("getFormattedQueryRecord", V1FieldRecords.class); - method.setAccessible(true); - @SuppressWarnings("unchecked") - HashMap result = (HashMap) method.invoke(null, record); - - // Both keys must be present — skyflow_id kept for v2 backward compat, skyflowId is the new form - Assert.assertEquals("skyflowId should be present (new form)", "xyz-456", result.get("skyflowId")); - Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "xyz-456", result.get("skyflow_id")); - Assert.assertEquals("other fields should be preserved", "test@example.com", result.get("email")); -} - -@Test -public void testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch() throws Exception { - Map tokens = new HashMap<>(); - tokens.put("skyflow_id", "tok-789"); - tokens.put("card_number", "tok-card-abc"); - V1FieldRecords record = V1FieldRecords.builder().tokens(tokens).build(); - - Method method = VaultController.class.getDeclaredMethod("getFormattedGetRecord", V1FieldRecords.class); - method.setAccessible(true); - @SuppressWarnings("unchecked") - HashMap result = (HashMap) method.invoke(null, record); - - // Both keys must be present in tokens branch too - Assert.assertEquals("skyflowId should be present (new form)", "tok-789", result.get("skyflowId")); - Assert.assertEquals("skyflow_id should still be present (v2 deprecated form)", "tok-789", result.get("skyflow_id")); - Assert.assertEquals("other token fields should be preserved", "tok-card-abc", result.get("card_number")); -} -``` - -- [ ] **Step 2: Run the tests to confirm they fail (skyflow_id is still being removed)** - -```bash -mvn test -pl . -Dtest=VaultControllerTests#testGetFormattedGetRecordNormalisesSkyflowId+testGetFormattedQueryRecordNormalisesSkyflowId+testGetFormattedGetRecordNormalisesSkyflowIdInTokensBranch -q 2>&1 | tail -20 -``` - -Expected: FAIL — assertions on `skyflow_id` being present fail because it is currently removed. - -- [ ] **Step 3: Update `getFormattedGetRecord` in VaultController.java** - -Change the rename block from a **move** to a **copy** and add a WARN log. The import `com.skyflow.logs.InfoLogs` is already present. - -Replace: -```java - if (getRecord.containsKey("skyflow_id")) { - getRecord.put("skyflowId", getRecord.remove("skyflow_id")); - } -``` - -With: -```java - if (getRecord.containsKey("skyflow_id")) { - getRecord.put("skyflowId", getRecord.get("skyflow_id")); - LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); - } -``` - -- [ ] **Step 4: Update `getFormattedQueryRecord` in VaultController.java** - -Replace: -```java - if (queryRecord.containsKey("skyflow_id")) { - queryRecord.put("skyflowId", queryRecord.remove("skyflow_id")); - } -``` - -With: -```java - if (queryRecord.containsKey("skyflow_id")) { - queryRecord.put("skyflowId", queryRecord.get("skyflow_id")); - LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); - } -``` - -- [ ] **Step 5: Run the tests to confirm they pass** - -```bash -mvn test -pl . -Dtest=VaultControllerTests -q 2>&1 | tail -20 -``` - -Expected: all 11 tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src/main/java/com/skyflow/vault/controller/VaultController.java \ - src/test/java/com/skyflow/vault/controller/VaultControllerTests.java -git commit -m "fix: restore skyflow_id key in Get/Query responses for v2 backward compat - -Both skyflow_id (deprecated, v2) and skyflowId (new form) are now present -in response maps simultaneously. WARN log emitted per record to signal -migration path to callers." -``` - ---- - -## Task 3: Add deprecation Javadoc to GetResponse and QueryResponse - -**Files:** -- Modify: `src/main/java/com/skyflow/vault/data/GetResponse.java` -- Modify: `src/main/java/com/skyflow/vault/data/QueryResponse.java` - -### Background -Customers reading `getData()` or `getFields()` should see a compiler-visible signal that `skyflow_id` is a deprecated key in the returned map, with guidance to migrate to `skyflowId`. - -- [ ] **Step 1: Add Javadoc to `GetResponse.getData()`** - -In `src/main/java/com/skyflow/vault/data/GetResponse.java`, add Javadoc above `getData()`: - -```java - /** - * Returns the list of record maps from the Get 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> getData() { - return data; - } -``` - -- [ ] **Step 2: Add Javadoc to `QueryResponse.getFields()`** - -In `src/main/java/com/skyflow/vault/data/QueryResponse.java`, add Javadoc above `getFields()`: - -```java - /** - * 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; - } -``` - -- [ ] **Step 3: Verify compilation** - -```bash -mvn compile -q 2>&1 | tail -10 -``` - -Expected: no output. - -- [ ] **Step 4: Commit** - -```bash -git add src/main/java/com/skyflow/vault/data/GetResponse.java \ - src/main/java/com/skyflow/vault/data/QueryResponse.java -git commit -m "docs: add deprecation Javadoc for skyflow_id key in GetResponse and QueryResponse" -``` - ---- - -## Task 4: Add deprecation WARN logs in BearerToken for old credential field names - -**Files:** -- Modify: `src/main/java/com/skyflow/serviceaccount/util/BearerToken.java:102-127` - -### Background -`getBearerTokenFromCredentials` already has fallback: tries `clientId` first, then `clientID`. When the fallback triggers (new form returns null, old form returns non-null), we emit a WARN log so users know to migrate their credentials file. - -- [ ] **Step 1: Add WARN logs to the three fallback paths in `BearerToken.java`** - -Current code (lines 102–127): -```java - // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration - JsonElement clientId = credentials.get("clientId"); - if (clientId == null) { - clientId = credentials.get("clientID"); - } - if (clientId == null) { ... throw ... } - - JsonElement keyId = credentials.get("keyId"); - if (keyId == null) { - keyId = credentials.get("keyID"); - } - if (keyId == null) { ... throw ... } - - JsonElement tokenUri = credentials.get("tokenUri"); - if (tokenUri == null) { - tokenUri = credentials.get("tokenURI"); - } - if (tokenUri == null) { ... throw ... } -``` - -Replace with — adding a WARN log inside each fallback `if` block: - -```java - // Accept both new-form keys (clientId/keyId/tokenUri) and legacy all-caps form for migration - JsonElement clientId = credentials.get("clientId"); - if (clientId == null) { - clientId = credentials.get("clientID"); - if (clientId != null) { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); - } - } - if (clientId == null) { - LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); - } - - JsonElement keyId = credentials.get("keyId"); - if (keyId == null) { - keyId = credentials.get("keyID"); - if (keyId != null) { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); - } - } - if (keyId == null) { - LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); - } - - JsonElement tokenUri = credentials.get("tokenUri"); - if (tokenUri == null) { - tokenUri = credentials.get("tokenURI"); - if (tokenUri != null) { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_TOKEN_URI.getLog()); - } - } - if (tokenUri == null) { - LogUtil.printErrorLog(ErrorLogs.TOKEN_URI_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingTokenUri.getMessage()); - } -``` - -- [ ] **Step 2: Verify compilation and run BearerToken tests** - -```bash -mvn compile -q 2>&1 | tail -5 -mvn test -pl . -Dtest=BearerTokenTests -q 2>&1 | tail -10 -``` - -Expected: clean compile, 17/17 tests pass. The existing test `testBearerTokenWithOldFormCredentialKeys` (which uses `clientID`/`keyID`/`tokenURI`) continues to pass — now with a WARN log emitted at runtime. - -- [ ] **Step 3: Commit** - -```bash -git add src/main/java/com/skyflow/serviceaccount/util/BearerToken.java -git commit -m "feat: emit deprecation WARN log when legacy clientID/keyID/tokenURI credential fields are used in BearerToken" -``` - ---- - -## Task 5: Add deprecation WARN logs in SignedDataTokens for old credential field names - -**Files:** -- Modify: `src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java:103-122` - -### Background -Same as Task 4 but for `SignedDataTokens`. Only `clientId`/`keyId` — no `tokenUri` in this class. - -- [ ] **Step 1: Add WARN logs to the two fallback paths in `SignedDataTokens.java`** - -Current code (lines 103–122): -```java - // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration - JsonElement clientId = credentials.get("clientId"); - if (clientId == null) { - clientId = credentials.get("clientID"); - } - if (clientId == null) { ... throw ... } - - JsonElement keyId = credentials.get("keyId"); - if (keyId == null) { - keyId = credentials.get("keyID"); - } - if (keyId == null) { ... throw ... } -``` - -Replace with: - -```java - // Accept both new-form keys (clientId/keyId) and legacy all-caps form for migration - JsonElement clientId = credentials.get("clientId"); - if (clientId == null) { - clientId = credentials.get("clientID"); - if (clientId != null) { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_CLIENT_ID.getLog()); - } - } - if (clientId == null) { - LogUtil.printErrorLog(ErrorLogs.CLIENT_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingClientId.getMessage()); - } - - JsonElement keyId = credentials.get("keyId"); - if (keyId == null) { - keyId = credentials.get("keyID"); - if (keyId != null) { - LogUtil.printWarningLog(InfoLogs.DEPRECATED_CREDENTIAL_KEY_ID.getLog()); - } - } - if (keyId == null) { - LogUtil.printErrorLog(ErrorLogs.KEY_ID_IS_REQUIRED.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.MissingKeyId.getMessage()); - } -``` - -Also verify that `InfoLogs` is already imported in `SignedDataTokens.java`. If not, add: -```java -import com.skyflow.logs.InfoLogs; -``` - -- [ ] **Step 2: Verify compilation and run SignedDataTokens tests** - -```bash -mvn compile -q 2>&1 | tail -5 -mvn test -pl . -Dtest=SignedDataTokensTests -q 2>&1 | tail -10 -``` - -Expected: clean compile, 15/15 tests pass. - -- [ ] **Step 3: Commit** - -```bash -git add src/main/java/com/skyflow/serviceaccount/util/SignedDataTokens.java -git commit -m "feat: emit deprecation WARN log when legacy clientID/keyID credential fields are used in SignedDataTokens" -``` - ---- - -## Task 6: Deprecate `downloadURL` → `downloadUrl` in GetRequest and DetokenizeRequest - -**Files:** -- Modify: `src/main/java/com/skyflow/vault/data/GetRequest.java` -- Modify: `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java` -- Modify: `src/main/java/com/skyflow/logs/InfoLogs.java` (add one entry) - -### Background -`getDownloadURL()` and builder `.downloadURL()` use all-caps `URL`, violating the same acronym-as-word rule as `clientID`/`tokenURI`. Since these are Java method names (not map keys), we use the standard `@Deprecated` annotation + Javadoc, which gives callers a **compile-time warning** in their IDE. No runtime `LogUtil` log is needed — the annotation is the industry standard signal for method deprecation. Keep the old methods as delegates to the new ones so existing code compiles without changes. - -- [ ] **Step 1: Add `DEPRECATED_DOWNLOAD_URL` to InfoLogs.java** - -Open `src/main/java/com/skyflow/logs/InfoLogs.java` and add one entry to the deprecation section: - -```java - DEPRECATED_DOWNLOAD_URL("[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead."), -``` - -- [ ] **Step 2: Write failing tests for new `downloadUrl` methods** - -Add these tests to `src/test/java/com/skyflow/vault/controller/VaultControllerTests.java` (or a new `GetRequestTest.java`): - -```java -import com.skyflow.vault.data.GetRequest; -import com.skyflow.vault.tokens.DetokenizeRequest; - -@Test -public void testGetRequestDownloadUrlNewForm() { - GetRequest request = GetRequest.builder() - .table("test_table") - .downloadUrl(true) - .build(); - Assert.assertTrue("downloadUrl(true) should be set", request.getDownloadUrl()); -} - -@Test -public void testGetRequestDownloadURLOldFormStillWorks() { - GetRequest request = GetRequest.builder() - .table("test_table") - .downloadURL(true) - .build(); - // Old method delegates to new — both accessors return the same value - Assert.assertTrue("old downloadURL() should still work", request.getDownloadURL()); - Assert.assertTrue("new getDownloadUrl() should also return same value", request.getDownloadUrl()); -} - -@Test -public void testDetokenizeRequestDownloadUrlNewForm() { - DetokenizeRequest request = DetokenizeRequest.builder() - .downloadUrl(true) - .build(); - Assert.assertTrue("downloadUrl(true) should be set", request.getDownloadUrl()); -} -``` - -- [ ] **Step 3: Run tests to confirm they fail** - -```bash -mvn test -pl . -Dtest=VaultControllerTests#testGetRequestDownloadUrlNewForm+testGetRequestDownloadURLOldFormStillWorks+testDetokenizeRequestDownloadUrlNewForm -q 2>&1 | tail -10 -``` - -Expected: compile error — `downloadUrl()` and `getDownloadUrl()` methods do not exist yet. - -- [ ] **Step 4: Update `GetRequest.java`** - -In `src/main/java/com/skyflow/vault/data/GetRequest.java`: - -**On the request class** — add new getter, mark old one `@Deprecated(since, forRemoval)`: - -Using `forRemoval = true` triggers an **orange underline** in IntelliJ/VS Code (stronger than plain `@Deprecated` yellow). The `{@link}` in Javadoc creates a clickable link to the new method in the IDE autocomplete tooltip, so the developer sees the replacement inline without leaving autocomplete. - -```java - /** - * @deprecated Use {@link #getDownloadUrl()} instead. - */ - @Deprecated(since = "2.1", forRemoval = true) - public Boolean getDownloadURL() { - return getDownloadUrl(); - } - - public Boolean getDownloadUrl() { - return this.builder.downloadUrl; - } -``` - -**On the builder** — rename the field and add both builder methods: -```java - public static final class GetRequestBuilder { - // ... other fields ... - private Boolean downloadUrl; // renamed from downloadURL - - /** - * @deprecated Use {@link #downloadUrl(Boolean)} instead. - */ - @Deprecated(since = "2.1", forRemoval = true) - public GetRequestBuilder downloadURL(Boolean downloadURL) { - return downloadUrl(downloadURL); - } - - public GetRequestBuilder downloadUrl(Boolean downloadUrl) { - this.downloadUrl = downloadUrl; - return this; - } - } -``` - -Also update the `getDownloadURL()` accessor in the request body (the non-builder getter) to delegate: -```java - public Boolean getDownloadURL() { - return getDownloadUrl(); - } - - public Boolean getDownloadUrl() { - return this.builder.downloadUrl; - } -``` - -Note: the existing `getDownloadURL()` in the request class (not the builder) currently reads `this.builder.downloadURL`. After renaming the field to `downloadUrl`, update the reference accordingly. - -- [ ] **Step 5: Update `DetokenizeRequest.java`** - -Apply the identical pattern in `src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java`: - -**On the request class:** -```java - /** - * @deprecated Use {@link #getDownloadUrl()} instead. - */ - @Deprecated(since = "2.1", forRemoval = true) - public Boolean getDownloadURL() { - return getDownloadUrl(); - } - - public Boolean getDownloadUrl() { - return this.builder.downloadUrl; - } -``` - -**On the builder:** -```java - private Boolean downloadUrl; // renamed from downloadURL - - /** - * @deprecated Use {@link #downloadUrl(Boolean)} instead. - */ - @Deprecated(since = "2.1", forRemoval = true) - public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { - return downloadUrl(downloadURL); - } - - public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { - this.downloadUrl = downloadUrl; - return this; - } -``` - -- [ ] **Step 6: Run the new tests to confirm they pass** - -```bash -mvn test -pl . -Dtest=VaultControllerTests#testGetRequestDownloadUrlNewForm+testGetRequestDownloadURLOldFormStillWorks+testDetokenizeRequestDownloadUrlNewForm -q 2>&1 | tail -10 -``` - -Expected: all 3 pass. - -- [ ] **Step 7: Run full suite to confirm no regressions** - -```bash -mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR" | tail -5 -``` - -Expected: baseline only (no new failures). - -- [ ] **Step 8: Commit** - -```bash -git add src/main/java/com/skyflow/vault/data/GetRequest.java \ - src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java \ - src/main/java/com/skyflow/logs/InfoLogs.java \ - src/test/java/com/skyflow/vault/controller/VaultControllerTests.java -git commit -m "feat: deprecate downloadURL in favour of downloadUrl in GetRequest and DetokenizeRequest - -Old downloadURL() methods kept as @Deprecated delegates for v2 backward -compat. New downloadUrl() methods follow the acronym-as-word convention -consistent with skyflowId, clientId, tokenUri." -``` - ---- - -## Task 8: Final verification — full test suite - -- [ ] **Step 1: Run full test suite** - -```bash -mvn test -q 2>&1 | grep -E "Tests run|FAIL|ERROR" | tail -10 -``` - -Expected baseline: 374 tests, ~5 failures, ~4 errors (all pre-existing — see `CLAUDE.md` for the list). No new failures. - -- [ ] **Step 2: Verify both keys appear in a sample response** - -Manually verify by running a grep to confirm the implementation is correct: - -```bash -grep -n "skyflow_id\|skyflowId\|DEPRECATED" src/main/java/com/skyflow/vault/controller/VaultController.java -``` - -Expected output includes lines like: -``` -getRecord.put("skyflowId", getRecord.get("skyflow_id")); -LogUtil.printWarningLog(InfoLogs.DEPRECATED_SKYFLOW_ID_KEY.getLog()); -``` - -and no `getRecord.remove("skyflow_id")`. - -- [ ] **Step 3: Commit (if any final cleanup needed)** - -```bash -git commit --allow-empty -m "chore: v2 backward compat + deprecation warnings complete" -``` diff --git a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md b/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md deleted file mode 100644 index 9873fdcf..00000000 --- a/docs/superpowers/specs/2026-05-13-java-nomenclature-cleanup-design.md +++ /dev/null @@ -1,133 +0,0 @@ -# Java SDK Nomenclature Cleanup — Design Spec - -**Date:** 2026-05-13 -**Reference:** [Skyflow Server-Side SDK: Nomenclature changes](https://skyflow.atlassian.net/wiki/spaces/SDK1/pages/2933162001/Skyflow+Server-Side+SDK+Nomenclature+changes) -**Scope:** Public interface only (`src/main/java/com/skyflow/`) -**Target release:** v3.0.0 (HIGH), v3.1.0 (MEDIUM), v3.1.x (LOW audit) - ---- - -## Summary of changes - -| Priority | Change | Files | -|---|---|---| -| HIGH | `clientID` → `clientId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | -| HIGH | `keyID` → `keyId` in credentials JSON parsing (with fallback) | `BearerToken.java`, `SignedDataTokens.java` | -| HIGH | `tokenURI` → `tokenUri` in credentials JSON parsing (with fallback) | `BearerToken.java` | -| MEDIUM | `skyflow_id` → `skyflowId` key in Get and Query response maps | `VaultController.java` | -| MEDIUM | `getErrors()` added to `QueryResponse` (field was missing entirely) | `QueryResponse.java` | -| LOW | Audit all builder setter/getter names — confirm no `setFooID()` pattern | `VaultConfig.java`, request builders | - ---- - -## Detailed design - -### HIGH: Credentials JSON field renames (`clientID` → `clientId`, `keyID` → `keyId`, `tokenURI` → `tokenUri`) - -**Affected:** `BearerToken.java` (`getBearerTokenFromCredentials`), `SignedDataTokens.java` (`generateSignedTokensFromCredentials`) - -**Why this change is needed:** - -Java's naming convention treats acronyms as ordinary word components in camelCase identifiers — `Id` not `ID`, `Uri` not `URI`. The current field names `clientID`, `keyID`, `tokenURI` violate this by capitalising the acronym in full. This is inconsistent with the rest of the SDK (e.g. `setVaultId()`, `setClusterId()`) and breaks the "principle of least surprise" for Java developers who expect `clientId`. - -These field names are defined in the credentials JSON file that users create and pass to the SDK (either as a file path or as a credentials string). They are therefore part of the SDK's public contract — a change forces users to update their credentials files. This is a breaking change, which is why it is gated to the v3.0.0 major release. - -**Why a fallback is used instead of a hard cut:** - -A hard cut would silently break all existing integrations the moment users upgrade to v3. The try-new-first fallback gives users a transition window: credentials files with the old keys continue to work, and users can migrate at their own pace. The fallback can be removed in a future major version once the old form is fully deprecated. - -**Implementation strategy:** Try the new key first; fall back to the old key if null; throw if both are absent. - -```java -// clientID → clientId -JsonElement clientId = credentials.get("clientId"); -if (clientId == null) clientId = credentials.get("clientID"); -if (clientId == null) { - throw new SkyflowException(...MissingClientId...); -} - -// keyID → keyId -JsonElement keyId = credentials.get("keyId"); -if (keyId == null) keyId = credentials.get("keyID"); -if (keyId == null) { - throw new SkyflowException(...MissingKeyId...); -} - -// tokenURI → tokenUri (BearerToken only — SignedDataTokens does not use tokenURI) -JsonElement tokenUri = credentials.get("tokenUri"); -if (tokenUri == null) tokenUri = credentials.get("tokenURI"); -if (tokenUri == null) { - throw new SkyflowException(...MissingTokenUri...); -} -``` - -Local variable names and private method parameter names are also updated to the new form (`clientId`, `keyId`, `tokenUri`) for internal consistency, though this has no effect on the public interface. - ---- - -### MEDIUM: `skyflow_id` → `skyflowId` in Get and Query response maps - -**Affected:** `VaultController.java` — `getFormattedGetRecord()` and `getFormattedQueryRecord()` - -**Why this change is needed:** - -The Skyflow REST API returns records with a `skyflow_id` field in snake_case — this is the wire format. The Java SDK is responsible for translating the wire format into language-idiomatic representations before handing data to callers. Java is a camelCase language, and the SDK already normalises `skyflow_id` to `skyflowId` in Insert and Update responses: - -- `getFormattedBatchInsertRecord`: `insertRecord.put("skyflowId", recordObject.get("skyflow_id").getAsString())` -- `getFormattedBulkInsertRecord`: `insertRecord.put("skyflowId", record.getSkyflowId().get())` -- `getFormattedUpdateRecord`: `updateTokens.put("skyflowId", skyflowId)` - -However, `getFormattedGetRecord` and `getFormattedQueryRecord` call `putAll(fieldsOpt.get())` which passes the raw API map directly through — including `skyflow_id` in snake_case. This inconsistency means that developers who write `record.get("skyflowId")` after a Get or Query call get `null`, while the same code works after an Insert or Update. It forces callers to know which operation produced the response just to read a single field. - -**Implementation:** After `putAll`, check for the raw API key and rename it: - -```java -if (record.containsKey("skyflow_id")) { - record.put("skyflowId", record.remove("skyflow_id")); -} -``` - -Applied in both `getFormattedGetRecord` and `getFormattedQueryRecord`. - ---- - -### MEDIUM: `getErrors()` added to `QueryResponse` - -**Affected:** `QueryResponse.java` - -**Why this change is needed:** - -All other response types in the SDK (`GetResponse`, `InsertResponse`, `UpdateResponse`, `FileUploadResponse`) expose a `getErrors()` method. `QueryResponse` is the only one that does not — the `errors` field is referenced only inside `toString()` as a hardcoded literal `null`: - -```java -responseObject.add("errors", null); -``` - -A caller who writes `queryResponse.getErrors()` gets a compile error because the method does not exist. This breaks the consistency contract that callers rely on when writing generic response-handling code across different vault operations. - -**Fix:** Add `private final ArrayList> errors` as a constructor field (always `null` — consistent with other response types that pass `null` when there are no errors) and expose it via `getErrors()`. The field will always be `null` for QueryResponse since the Query API does not currently model partial-error responses the same way batch insert does. This is kept as `null` rather than an empty list to stay consistent with the existing pattern across other response classes. - ---- - -### LOW: Audit builder setter/getter names - -**Affected:** `VaultConfig.java`, `InsertRequest`, `UpdateRequest`, `GetRequest`, `DeleteRequest`, `FileUploadRequest`, `QueryRequest` - -**Why this change is needed:** - -The same acronym-casing rule that applies to credentials fields applies to all Java method names. Any setter or getter using `ID` (all-caps) as a suffix — e.g. `setVaultID()`, `getSkyflowID()` — is non-idiomatic and inconsistent with Java convention. The spec item 15 calls out this as a verification task. - -From initial review, `setVaultId()` and `setClusterId()` in `VaultConfig` are already correct. A full grep audit across all request builder classes is required to confirm there are no remaining `setFooID()` / `getFooID()` methods that were missed. - -**Outcome:** If any violations are found, rename them to `setFooId()` / `getFooId()`. If none are found, this item is closed as verified-clean. - ---- - -## What is NOT in scope - -- **`tokenizedData` in QueryResponse:** The Skyflow Query API explicitly cannot return tokens. The existing `toString()` hack that injects `tokenizedData: {}` is a minor inconsistency between string output and programmatic access, but since callers have no reason to access tokenized data from a query result, this is not worth fixing now. - -- **`UpdateRequest.getData()` map key**: Users currently pass `skyflow_id` (snake_case) in the data map to identify the record to update. This is an *input* key consumed by the SDK internally (`updateRequest.getData().remove("skyflow_id")`), not a response field surfaced to callers. The spec does not address this and changing it would require a separate design decision. -- **Generated REST client code** under `com.skyflow.generated.*`: These files are auto-generated by Fern from the API definition. Manual edits would be overwritten on the next regeneration. -- **`SKYFLOW_CREDENTIALS` environment variable name**: Stays `ALL_CAPS` per OS and shell convention. Only the parsed field names within the JSON value change. -- **Validation logic for null/None insert values**: The spec marks this as Python-only (item 12). Java already throws on invalid input at the API boundary. From a08edf97afde9144936625e99a78d448230caf06 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:34:26 +0530 Subject: [PATCH 38/48] =?UTF-8?q?chore:=20ignore=20docs/superpowers/=20?= =?UTF-8?q?=E2=80=94=20keep=20planning=20docs=20local=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d5a178d4..0efc6bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .idea -target \ No newline at end of file +target + +RUNNING_SAMPLES.md + +docs/superpowers/ From 566d9a956a5065be21d00bbb4d5fc0880264a3d6 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:45:27 +0530 Subject: [PATCH 39/48] =?UTF-8?q?chore:=20update=20cspell=20config=20?= =?UTF-8?q?=E2=80=94=20British=20English=20words,=20Maven=20flags,=20ignor?= =?UTF-8?q?e=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added words: serialise/d/s, normalise/d/s/Normalises, behaviour/s/Behaviour, sanitisation, recognised, unrecognised, prioritised Added regex: /-D[A-Za-z][A-Za-z0-9.]*/g to ignore Maven -D flags Added ignorePaths: RUNNING_SAMPLES.md, docs/superpowers/** Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .cspell.json | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.cspell.json b/.cspell.json index a0ec0be6..abe743cb 100644 --- a/.cspell.json +++ b/.cspell.json @@ -73,9 +73,26 @@ "pkcs", "prioritise", "Prioritise", + "prioritised", "Timeto", "Wdex", - "jacoco" + "jacoco", + "serialise", + "serialised", + "serialises", + "serialising", + "normalise", + "Normalise", + "normalised", + "normalises", + "Normalises", + "normalising", + "behaviour", + "Behaviour", + "behaviours", + "sanitisation", + "recognised", + "unrecognised" ], "languageSettings": [ { @@ -98,7 +115,9 @@ "src/main/java/com/skyflow/generated/**", "**/*.ts", "**/processed-*", - "samples/src/main/java/com/example/credentials.json" + "samples/src/main/java/com/example/credentials.json", + "RUNNING_SAMPLES.md", + "docs/superpowers/**" ], "ignoreRegExpList": [ "/\\b[A-Z][A-Z0-9_]{2,}\\b/g", @@ -106,6 +125,7 @@ "/(eyJ[A-Za-z0-9+/=_-]+\\.)+[A-Za-z0-9+/=_-]+/g", "/[A-Za-z0-9_.~-]*%[0-9A-Fa-f]{2}[A-Za-z0-9_.~%-]*/g", "/\\b[A-Za-z0-9_]{7,}\\b(?=])/g", - "/\"[A-Za-z0-9+/=]{15,}\"/g" + "/\"[A-Za-z0-9+/=]{15,}\"/g", + "/-D[A-Za-z][A-Za-z0-9.]*/g" ] } From 573433372bcd7cee4b62e040dfd4e92b57a29e8c Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:48:15 +0530 Subject: [PATCH 40/48] chore: remove v2-public-interface-changes.md Co-Authored-By: Claude Sonnet 4.6 (1M context) --- docs/v2-public-interface-changes.md | 218 ---------------------------- 1 file changed, 218 deletions(-) delete mode 100644 docs/v2-public-interface-changes.md diff --git a/docs/v2-public-interface-changes.md b/docs/v2-public-interface-changes.md deleted file mode 100644 index feb14413..00000000 --- a/docs/v2-public-interface-changes.md +++ /dev/null @@ -1,218 +0,0 @@ -# Skyflow Java SDK — Public Interface Changes & Deprecation Notice - -**Audience:** Product Managers, Technical Program Managers, Customer Success -**SDK:** skyflow-java -**Affected versions:** v2.x (current) → upcoming release - ---- - -## Overview - -As part of aligning the Skyflow Java SDK with cross-language naming standards, a set of public-facing field names and response keys are being updated. All changes are designed to be **non-breaking for existing customers** — old forms continue to work alongside new ones. - -Where applicable, deprecation warnings are logged at runtime or signalled at compile time to guide migration. Credential JSON field names (`clientID`, `keyID`, `tokenURI`) are permanently supported alongside the new forms — no migration required. - ---- - -## How Deprecation Signals Work in Java IDEs - -Customers using modern Java IDEs (IntelliJ IDEA, VS Code, Eclipse) will see the following signals when using deprecated methods or fields. No code changes are required to see these — they appear automatically. - -### Method deprecation (`downloadURL` → `downloadUrl`) - -When a customer types `.downloadU` in their IDE, autocomplete shows both forms simultaneously. The old form is visually marked: - -``` -▼ Autocomplete -────────────────────────────────────────────────── - downloadUrl(Boolean) ← new form, no marker - ~~downloadURL~~(Boolean) ⚠️ ← strikethrough + warning icon -────────────────────────────────────────────────── -``` - -Hovering over the deprecated method shows an inline tooltip: -``` -⚠️ Deprecated. Use downloadUrl(Boolean) instead. -``` - -If a customer selects the deprecated form and uses it in their code, the IDE shows an **orange underline** at the call site — a stronger visual than a plain yellow warning — because the method is marked `forRemoval = true`. - -### Runtime log warnings (`skyflow_id` key) - -For map key changes that cannot use Java annotations, a `[DEPRECATED]` warning is logged at runtime when the old key is accessed: - -``` -[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed -in an upcoming release. Use 'skyflowId' instead. -``` - -These appear in the application log at WARN level. Customers running with `LogLevel.WARN` or higher will see them. - ---- - ---- - -## What Is Changing - -### 1. Credentials file field names - -When customers authenticate using a service account credentials JSON file, the field names inside that file are changing to follow Java naming conventions (lowercase acronyms). - -| Old field name | New field name | Used in | -|---|---|---| -| `clientID` | `clientId` | `credentials.json` | -| `keyID` | `keyId` | `credentials.json` | -| `tokenURI` | `tokenUri` | `credentials.json` | - -**Customer impact:** Both old and new field names are permanently supported — existing credentials files require no changes. No deprecation warning is emitted. Customers may migrate to the new names at any time but are not required to. - ---- - -### 2. Response field key in Get and Query operations - -When customers retrieve records from a vault using the **Get** or **Query** operations, each record includes a `skyflow_id` field identifying the record. This key name is changing to follow Java camelCase conventions. - -| Old key (deprecated) | New key | Affected operations | -|---|---|---| -| `skyflow_id` | `skyflowId` | Get, Query | - -**Customer impact:** Both `skyflow_id` and `skyflowId` will be present in response records simultaneously. Customers accessing `skyflow_id` today continue to receive the correct value. A deprecation warning will be logged once per record to prompt migration. - -**Example of the warning customers will see in their logs:** -``` -[DEPRECATED] Response key 'skyflow_id' is deprecated and will be removed in an upcoming release. Use 'skyflowId' instead. -``` - -> **Note:** Insert and Update operations already return `skyflowId` (camelCase) and are unaffected. - ---- - -### 3. `downloadURL` method names in Get and Detokenize operations - -Two method names used when configuring Get and Detokenize requests are changing to follow the same naming convention as the other fields above (`URL` → `Url`). - -| Old method (deprecated) | New method | Used in | -|---|---|---| -| `.downloadURL(true)` builder method | `.downloadUrl(true)` | `GetRequest.builder()`, `DetokenizeRequest.builder()` | -| `.getDownloadURL()` | `.getDownloadUrl()` | `GetRequest`, `DetokenizeRequest` | - -**Customer impact:** Existing code using `.downloadURL()` or `.getDownloadURL()` continues to compile and work. IDEs that support Java `@Deprecated` annotation will show a visual strikethrough on the old method name as a migration hint. No runtime behavior changes. - -**Example of the IDE/compiler warning customers will see:** -``` -[DEPRECATED] Method 'downloadURL()' is deprecated and will be removed in an upcoming release. Use 'downloadUrl()' instead. -``` - ---- - ---- - -## Behaviour Change: Insert and Update field value validation removed - -**Affected operations:** Insert, Update - -Previously the Java SDK threw an error if a record field value was `null` or an empty string `""`. This validation was inconsistent with both the Skyflow API spec (which accepts `additionalProperties: Any type`) and with other SDKs (Node has no such validation). - -**The validation has been removed.** Field values of `null`, `""`, or any type are now passed through to the backend unchanged. The backend is the authoritative source for field-level validation. - -| Scenario | Before | After | -|---|---|---| -| `{"name": null}` | SDK throws `SkyflowException` | Passed to BE ✓ | -| `{"name": ""}` | SDK throws `SkyflowException` | Passed to BE ✓ | -| `records: []` (empty array) | SDK throws `SkyflowException` | Passed to BE ✓ | - -**This is a non-breaking change** — code that was previously failing will now succeed. - ---- - -## What Is NOT Changing - -- The Java method names customers call (e.g. `.insert()`, `.get()`, `.query()`) -- The request builder APIs (e.g. `InsertRequest.builder()`) -- Any vault configuration APIs (`VaultConfig`, `Credentials` setters) -- Authentication behaviour — credentials files still work identically -- Any connection, detect, audit, or tokenize interfaces - ---- - -## Deprecation Strategy - -| Phase | What happens | Timeline | -|---|---|---| -| **Now (v2.x)** | Old forms still work. Deprecation `[DEPRECATED]` warning logged at WARN level when old form is used. New forms also accepted. | Current | -| **Upcoming release** | Old forms removed. Only new forms accepted. Customers who have not migrated will see errors. | TBD | - -Customers can suppress deprecation warnings by updating to the new field names at any time — no other code changes are required. - ---- - -## Customer Migration Guide - -### Get / Detokenize `downloadURL` → `downloadUrl` - -```java -// Before (deprecated — still compiles in v2, removed in upcoming release) -GetRequest request = GetRequest.builder() - .table("persons") - .ids(ids) - .downloadURL(true) // ← deprecated - .build(); - -// After -GetRequest request = GetRequest.builder() - .table("persons") - .ids(ids) - .downloadUrl(true) // ← new form - .build(); -``` - -### Credentials file - -Update `credentials.json`: - -```json -// Before (deprecated) -{ - "clientID": "...", - "keyID": "...", - "tokenURI": "...", - "privateKey": "..." -} - -// After (new — no other changes needed) -{ - "clientId": "...", - "keyId": "...", - "tokenUri": "...", - "privateKey": "..." -} -``` - -### Get / Query response access - -```java -// Before (deprecated — still works in v2, removed in upcoming release) -String id = record.get("skyflow_id").toString(); - -// After (new form — works in current and all future versions) -String id = record.get("skyflowId").toString(); -``` - ---- - -## How to Check If Your Integration Is Affected - -Set log level to `WARN` or higher. If you see any `[DEPRECATED]` entries in your application logs after upgrading, your integration is using an old form and should be updated before the next major release. - -```java -Skyflow client = Skyflow.builder() - .setLogLevel(LogLevel.WARN) - ... - .build(); -``` - ---- - -## Questions - -For technical questions, contact the SDK team. For release timeline questions, contact your Skyflow account representative. From 5acbb849d7f6831197a8a057cbbf5753115631df Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 16:56:03 +0530 Subject: [PATCH 41/48] fix: replace real RSA key with fake key in BearerTokenTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real 2048-bit RSA key replaced with fake base64 value. Assertion updated from InvalidTokenUri to InvalidKeySpec — still proves all credential fields were resolved (failure is at RSA parsing, not field lookup). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../skyflow/serviceaccount/util/BearerTokenTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java index 77a1810a..4eb90458 100644 --- a/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java +++ b/src/test/java/com/skyflow/serviceaccount/util/BearerTokenTests.java @@ -3,6 +3,7 @@ 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 org.junit.Assert; import org.junit.BeforeClass; @@ -253,15 +254,18 @@ public void testInvalidTokenURIInCredentialsForCredentials() throws SkyflowExcep @Test public void testBearerTokenWithNewFormCredentialKeys() { try { - String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzLp0TVwidRMtZ\\n4tGLHPDEF6ihmE4OHSR/r5rZGqE+PNtw/uwXzBrfz1Mktb0hddMZNwC2IKhHE0Yw\\nvtBT0jsfy4OUQR13Mohn9znz+5TES/yXjkvZjhZKzs5rxNw/cO8lpKYUYdwbFzwl\\n9e3joCsWBXBDCbXdLQGPyggJV+KBI0LBal+LngNLU/U680LRlybCKCTyyrF0SERD\\npytcpnq41CS2Q0ZDfkK/zLrvsCkEBU8xYeAf/TphXMKeqvMGTqxxg6IPOKfYya7Q\\nnH9eZ1pn1SCe6N5XBUpQpB4K+1IZKvadOYpYWzRgM+tT5k4UVsg6s7kUm8k9n85/\\nNQMjMY2XAgMBAAECggEASlg05ClgcaBxn0H1H3tKipImbaX7/O8qjbAW162s6V3m\\nzuN2ogkVvXcQUFL3vkJc7EFeEjNKnvLoVKFXXvADiBWw6np591MINdrmOM1R1ICS\\ntW9dGU9TAIb+LsjneYsqLrw6DIruAG+LjVSU97UlK2XmRmppAvQBid+Rpg7I9Dsy\\naJyGjDHeC3RyYYNfpei2dBPUYlUjOkBqgYGOOyjYxHzzgYtdVZku0JPtsAey3WKL\\nSbu8ryugu7r23fxP50H3FtYz91TPlVu1zVEk9Viizp2c9642ZKEoA0bB/bSNMUnt\\nZ/kemZENAzC7tnoYgwN09rI3h0+U5jaU1BhXbrLpAQKBgQDt8eaywv6j+Hdv8i7S\\nyMnZE4CaM70Z319ctJPlt2QdCZp8dtac858qnnrrZSCWV3n3yMv//bf1WZB4Lssw\\nuxBzSCFI/imG6eY9uQA6yXLl1TY9DA5IJ8s2LGzwmtA1q+vC+jzWs+0+S/evUewo\\nTZGQuNjHMHoM22jeLErqQZkHUQKBgQDAxz1WY56ZHdC3Y4aXkDeb5Ag+ZJV8Uqwn\\nootA2zHCaEx8gM9CzChCl4pQcghHFXv4eEKqezdWSK+SIRA1CtR+q8g5dP8YtAkR\\n9Uav6/fEkM8iCUvhZg+1DPRShu15nQF0ZAleSJ9OiSW5pIfAbY79RHru8H31azhE\\nDOWezXbcZwKBgB9LAAckg+62n6aWWDgadglZekFNaqI7cUQ073p3mvACslGKI4Fy\\nvM0TGKFapGWBTaYbv1CEYqwewlQ7+zcGcwxmQRJjcryuiDw312Lj2XuGheKTclFl\\nAmG2iAFAqv9UA+aZmGS4NwxJW2KwSHmocetxk/jmVDbaqDkH5DZYuDJxAoGBAJqn\\n/PRujVEnk0dc6CB1ybcd9OMhTK/ln0lY5MDOWRgvFpWXvS9InE/4RTWOlkd42/EV\\ngd5FZbqqK3hfYCI9owZQiBxYWUMXRGOM0/3Un/ypdBNJQ//7IkTMtMH0j1XOeNlI\\nXB+wwWV/L63EakgdXOag5sMEWvjl4MjvU9PX4DCnAoGAR0c567DWbkTXvcNIjvNF\\nNK8suq/fGt4dpbkkFOEHjgqFd5RsjFHKc98JVrudPweUR7YjpeKQaeNKXfVFd4+N\\nDPOs0zWSsaHckh1g9djkZlidha9SD/V6cOpxi3g2okcn/LI7h8NyNlAwDSn2mPEi\\nMd3mrgMCZwJsXLndGQSDVUw=\\n-----END PRIVATE KEY-----\\n\", " + // Fake key — fails at RSA parsing, not at field lookup, confirming new-form keys were accepted + String credentialsString = "{\"privateKey\": \"-----BEGIN PRIVATE KEY-----\\ncHJpdmF0ZV9rZXlfdmFsdWU=\\n-----END PRIVATE KEY-----\", " + "\"clientId\": \"client_id_value\", \"keyId\": \"key_id_value\", \"tokenUri\": \"invalid_token_uri\"}"; BearerToken bearerToken = BearerToken.builder().setCredentials(credentialsString).build(); bearerToken.getBearerToken(); Assert.fail(EXCEPTION_NOT_THROWN); } catch (SkyflowException e) { Assert.assertEquals(ErrorCode.INVALID_INPUT.getCode(), e.getHttpCode()); - // InvalidTokenUri means new-form keys were resolved successfully — failure is at URL parsing, not field lookup - Assert.assertEquals(ErrorMessage.InvalidTokenUri.getMessage(), e.getMessage()); + // 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), + e.getMessage()); } } } From a8026686fa071419e52fea438f97846845de9618 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 17:37:31 +0530 Subject: [PATCH 42/48] fix: guard against null in DetokenizeRequest.downloadUrl(null) Calling .downloadUrl(null) previously stored null in the field, creating an NPE risk for callers who read getDownloadUrl() back without a null check. Now null -> false (matching the default), consistent with the continueOnError(null) guard in the same builder. Added test: testDetokenizeRequestDownloadUrlNullTreatedAsFalse Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../java/com/skyflow/vault/tokens/DetokenizeRequest.java | 2 +- .../skyflow/vault/controller/VaultControllerTests.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 481c0c16..8d9509e2 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -67,7 +67,7 @@ public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { } public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { - this.downloadUrl = downloadUrl; + this.downloadUrl = downloadUrl != null && downloadUrl; return this; } diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 5dc02f37..81296025 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -291,4 +291,12 @@ public void testDetokenizeRequestDownloadUrlDefaultIsFalse() { Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl()); } + @Test + public void testDetokenizeRequestDownloadUrlNullTreatedAsFalse() { + DetokenizeRequest request = DetokenizeRequest.builder() + .downloadUrl(null) + .build(); + Assert.assertFalse("null downloadUrl should default to false — no NPE risk", request.getDownloadUrl()); + } + } From 40b53dec738d0ac2203bf773618292fc9658ddd9 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 17:41:12 +0530 Subject: [PATCH 43/48] chore: remove dead error constants after validation removal EmptyValues, EmptyValueInValues, EmptyValueInTokens (ErrorMessage) and EMPTY_VALUES, EMPTY_OR_NULL_VALUE_IN_VALUES, EMPTY_OR_NULL_VALUE_IN_TOKENS (ErrorLogs) are unreachable since the SDK-level null/empty field validation was removed. Deleted to prevent accidental re-wiring. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/main/java/com/skyflow/errors/ErrorMessage.java | 3 --- src/main/java/com/skyflow/logs/ErrorLogs.java | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/main/java/com/skyflow/errors/ErrorMessage.java b/src/main/java/com/skyflow/errors/ErrorMessage.java index fc222522..a99dd1cc 100644 --- a/src/main/java/com/skyflow/errors/ErrorMessage.java +++ b/src/main/java/com/skyflow/errors/ErrorMessage.java @@ -58,13 +58,10 @@ public enum ErrorMessage { 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."), - 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."), 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."), diff --git a/src/main/java/com/skyflow/logs/ErrorLogs.java b/src/main/java/com/skyflow/logs/ErrorLogs.java index e6e8b304..47866efc 100644 --- a/src/main/java/com/skyflow/logs/ErrorLogs.java +++ b/src/main/java/com/skyflow/logs/ErrorLogs.java @@ -50,15 +50,12 @@ 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."), - 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."), 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."), From bff8b526f645e39a751d8c443394118c51894587 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 17:42:40 +0530 Subject: [PATCH 44/48] Revert "fix: guard against null in DetokenizeRequest.downloadUrl(null)" This reverts commit a8026686fa071419e52fea438f97846845de9618. --- .../java/com/skyflow/vault/tokens/DetokenizeRequest.java | 2 +- .../skyflow/vault/controller/VaultControllerTests.java | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java index 8d9509e2..481c0c16 100644 --- a/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java +++ b/src/main/java/com/skyflow/vault/tokens/DetokenizeRequest.java @@ -67,7 +67,7 @@ public DetokenizeRequestBuilder downloadURL(Boolean downloadURL) { } public DetokenizeRequestBuilder downloadUrl(Boolean downloadUrl) { - this.downloadUrl = downloadUrl != null && downloadUrl; + this.downloadUrl = downloadUrl; return this; } diff --git a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java index 81296025..5dc02f37 100644 --- a/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java +++ b/src/test/java/com/skyflow/vault/controller/VaultControllerTests.java @@ -291,12 +291,4 @@ public void testDetokenizeRequestDownloadUrlDefaultIsFalse() { Assert.assertFalse("downloadUrl should be false by default", request.getDownloadUrl()); } - @Test - public void testDetokenizeRequestDownloadUrlNullTreatedAsFalse() { - DetokenizeRequest request = DetokenizeRequest.builder() - .downloadUrl(null) - .build(); - Assert.assertFalse("null downloadUrl should default to false — no NPE risk", request.getDownloadUrl()); - } - } From ee167b4718933c6c13af96e773dc6dd0b5d3003d Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Mon, 18 May 2026 17:44:50 +0530 Subject: [PATCH 45/48] test: add positive tests for permissive Insert validation behaviour Three new tests assert that previously-blocked inputs now pass SDK validation (SDK defers to BE per API spec additionalProperties: Any type): - Empty values array [] passes - Null field value passes - Empty string field value passes Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../com/skyflow/vault/data/InsertTests.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/com/skyflow/vault/data/InsertTests.java b/src/test/java/com/skyflow/vault/data/InsertTests.java index cc7731be..a9178f4a 100644 --- a/src/test/java/com/skyflow/vault/data/InsertTests.java +++ b/src/test/java/com/skyflow/vault/data/InsertTests.java @@ -171,6 +171,43 @@ public void testNoValuesInInsertRequestValidations() { } } + @Test + public void testEmptyValuesArrayPassesInsertRequestValidations() { + // SDK no longer blocks empty values array — BE is authoritative + InsertRequest request = InsertRequest.builder().table(table).values(values).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail("Empty values array should pass SDK validation: " + e.getMessage()); + } + } + + @Test + public void testNullFieldValuePassesInsertRequestValidations() { + // SDK no longer blocks null field values — BE is authoritative per API spec + valueMap.put("test_column_1", null); + values.add(valueMap); + InsertRequest request = InsertRequest.builder().table(table).values(values).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail("Null field value should pass SDK validation: " + e.getMessage()); + } + } + + @Test + public void testEmptyStringFieldValuePassesInsertRequestValidations() { + // SDK no longer blocks empty string field values — BE is authoritative per API spec + valueMap.put("test_column_1", ""); + values.add(valueMap); + InsertRequest request = InsertRequest.builder().table(table).values(values).build(); + try { + Validations.validateInsertRequest(request); + } catch (SkyflowException e) { + Assert.fail("Empty string field value should pass SDK validation: " + e.getMessage()); + } + } + @Test public void testEmptyKeyInValuesInInsertRequestValidations() { valueMap.put("", "test_value_3"); From 6b6c25d56540ced579481731f6e998ff82da4441 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Tue, 19 May 2026 11:59:28 +0530 Subject: [PATCH 46/48] fix banner --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31a9bb7d..e58b6fc7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Skyflow Java -> **Java V2.1.x IS NOW AVAILABLE:** A new, improved version of the Skyflow SDK is ready with flexible authentication, multi-vault support, builder patterns, and richer error diagnostics. V1 is in maintenance mode (security patches only) and will reach End of Life on October 31, 2026. We recommend upgrading to v2 — see the **[Migration Guide](docs/migrate_to_v2.md)** for step-by-step instructions. +> **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](add link)** 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. From 48d08c842adc87e07548dee1925ef07e46e3547a Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Tue, 19 May 2026 13:06:27 +0530 Subject: [PATCH 47/48] =?UTF-8?q?feat:=20port=20PR=20#273=20changes=20?= =?UTF-8?q?=E2=80=94=20raw=20body=20support,=20URL=20encoding,=20null-safe?= =?UTF-8?q?=20request=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add EMPTY_STRING, QUOTE, HTTPS_PROTOCOL, CURLY_PLACEHOLDER and HttpUtilityExtra (RAW_BODY_KEY, SDK_GENERATED_PREFIX) to Constants - HttpUtility: conditional content-type header, __raw_body__ passthrough, UUID fallback when server omits x-request-id, URL-encoded form params - Utils: URL-encode path and query params with graceful fallback - Validations: accept String request bodies for non-JSON content types - ConnectionController: wrap String bodies in __raw_body__ for non-JSON content types; fall back to raw string when response is not JSON - InfoLogs: "Bearer token is expired" → "Bearer token is invalid or expired" - HttpUtilityTests: add raw body, no content-type, null request ID and special-character form-encoding tests Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/skyflow/logs/InfoLogs.java | 2 +- .../java/com/skyflow/utils/Constants.java | 10 +++ .../java/com/skyflow/utils/HttpUtility.java | 27 ++++++-- src/main/java/com/skyflow/utils/Utils.java | 17 ++++- .../utils/validations/Validations.java | 28 ++++++-- .../controller/ConnectionController.java | 33 ++++++++-- .../com/skyflow/utils/HttpUtilityTests.java | 65 +++++++++++++++++++ 7 files changed, 162 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/skyflow/logs/InfoLogs.java b/src/main/java/com/skyflow/logs/InfoLogs.java index 6d745b9e..4391f1b1 100644 --- a/src/main/java/com/skyflow/logs/InfoLogs.java +++ b/src/main/java/com/skyflow/logs/InfoLogs.java @@ -14,7 +14,7 @@ public enum InfoLogs { // Bearer token generation EMPTY_BEARER_TOKEN("Bearer token is empty."), - BEARER_TOKEN_EXPIRED("Bearer token is expired."), + BEARER_TOKEN_EXPIRED("Bearer token is invalid or expired."), GET_BEARER_TOKEN_TRIGGERED("getBearerToken method triggered."), GET_BEARER_TOKEN_SUCCESS("Bearer token generated."), GET_SIGNED_DATA_TOKENS_TRIGGERED("getSignedDataTokens method triggered."), diff --git a/src/main/java/com/skyflow/utils/Constants.java b/src/main/java/com/skyflow/utils/Constants.java index 6732ea60..1162b3f0 100644 --- a/src/main/java/com/skyflow/utils/Constants.java +++ b/src/main/java/com/skyflow/utils/Constants.java @@ -34,6 +34,16 @@ public final class Constants { 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 RAW_BODY_KEY = "__raw_body__"; + public static final String SDK_GENERATED_PREFIX = "SDK-Generated-"; + private HttpUtilityExtra() {} + } static { String sdkVersion; diff --git a/src/main/java/com/skyflow/utils/HttpUtility.java b/src/main/java/com/skyflow/utils/HttpUtility.java index b8e9283b..e5793129 100644 --- a/src/main/java/com/skyflow/utils/HttpUtility.java +++ b/src/main/java/com/skyflow/utils/HttpUtility.java @@ -7,11 +7,13 @@ import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; public final class HttpUtility { @@ -32,8 +34,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< try { connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(method); - connection.setRequestProperty("content-type", "application/json"); connection.setRequestProperty("Accept", "*/*"); + boolean hasContentType = headers != null && headers.containsKey("content-type"); + if (!hasContentType && params != null && !params.isEmpty()) { + connection.setRequestProperty("content-type", "application/json"); + } if (headers != null && !headers.isEmpty()) { for (Map.Entry entry : headers.entrySet()) @@ -52,9 +57,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< byte[] input = null; String requestContentType = connection.getRequestProperty("content-type"); - if (requestContentType.contains("application/x-www-form-urlencoded")) { + if (params.has(Constants.HttpUtilityExtra.RAW_BODY_KEY) && params.size() == 1) { + input = params.get(Constants.HttpUtilityExtra.RAW_BODY_KEY).getAsString().getBytes(StandardCharsets.UTF_8); + } else if (requestContentType != null && requestContentType.contains("application/x-www-form-urlencoded")) { input = formatJsonToFormEncodedString(params).getBytes(StandardCharsets.UTF_8); - } else if (requestContentType.contains("multipart/form-data")) { + } else if (requestContentType != null && requestContentType.contains("multipart/form-data")) { input = formatJsonToMultiPartFormDataString(params, boundary).getBytes(StandardCharsets.UTF_8); } else { input = params.toString().getBytes(StandardCharsets.UTF_8); @@ -67,7 +74,11 @@ public static String sendRequest(String method, URL url, JsonObject params, Map< int httpCode = connection.getResponseCode(); String requestID = connection.getHeaderField("x-request-id"); - HttpUtility.requestID = requestID.split(",")[0]; + if (requestID != null) { + HttpUtility.requestID = requestID.split(",")[0]; + } else { + HttpUtility.requestID = Constants.HttpUtilityExtra.SDK_GENERATED_PREFIX + UUID.randomUUID(); + } Map> responseHeaders = connection.getHeaderFields(); Reader streamReader; if (httpCode > 299) { @@ -159,7 +170,13 @@ public static String appendRequestId(String message, String requestId) { } private static String makeFormEncodeKeyValuePair(String key, String value) { - return key + "=" + value + "&"; + try { + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()); + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + return encodedKey + "=" + encodedValue + "&"; + } catch (Exception e) { + return key + "=" + value + "&"; + } } } diff --git a/src/main/java/com/skyflow/utils/Utils.java b/src/main/java/com/skyflow/utils/Utils.java index b33b08c1..0c20bd6c 100644 --- a/src/main/java/com/skyflow/utils/Utils.java +++ b/src/main/java/com/skyflow/utils/Utils.java @@ -17,6 +17,8 @@ import java.io.File; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; @@ -119,7 +121,12 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne for (Map.Entry entry : invokeConnectionRequest.getPathParams().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - filledURL = new StringBuilder(filledURL.toString().replace(String.format("{%s}", key), value)); + try { + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), encodedValue)); + } catch (Exception e) { + filledURL = new StringBuilder(filledURL.toString().replace(String.format(Constants.CURLY_PLACEHOLDER, key), value)); + } } } @@ -128,7 +135,13 @@ public static String constructConnectionURL(ConnectionConfig config, InvokeConne for (Map.Entry entry : invokeConnectionRequest.getQueryParams().entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - filledURL.append(key).append("=").append(value).append("&"); + try { + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.name()); + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + filledURL.append(encodedKey).append("=").append(encodedValue).append("&"); + } catch (Exception e) { + filledURL.append(key).append("=").append(value).append("&"); + } } filledURL = new StringBuilder(filledURL.substring(0, filledURL.length() - 1)); } diff --git a/src/main/java/com/skyflow/utils/validations/Validations.java b/src/main/java/com/skyflow/utils/validations/Validations.java index d05ecfdd..0a4f847c 100644 --- a/src/main/java/com/skyflow/utils/validations/Validations.java +++ b/src/main/java/com/skyflow/utils/validations/Validations.java @@ -10,6 +10,7 @@ import java.util.regex.Pattern; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.skyflow.config.ConnectionConfig; import com.skyflow.config.Credentials; @@ -146,12 +147,27 @@ public static void validateInvokeConnectionRequest(InvokeConnectionRequest invok } if (requestBody != null) { - Gson gson = new Gson(); - JsonObject bodyObject = gson.toJsonTree(requestBody).getAsJsonObject(); - if (bodyObject.isEmpty()) { - LogUtil.printErrorLog(Utils.parameterizedString( - ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + if (requestBody.getClass().equals(Object.class)) { + return; + } + if (requestBody instanceof String) { + String bodyStr = (String) requestBody; + if (bodyStr.trim().isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + } + } else { + Gson gson = new Gson(); + JsonElement bodyElement = gson.toJsonTree(requestBody); + if (bodyElement.isJsonObject()) { + JsonObject bodyObject = bodyElement.getAsJsonObject(); + if (bodyObject.isEmpty()) { + LogUtil.printErrorLog(Utils.parameterizedString( + ErrorLogs.EMPTY_REQUEST_BODY.getLog(), InterfaceName.INVOKE_CONNECTION.getName())); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.EmptyRequestBody.getMessage()); + } + } } } } diff --git a/src/main/java/com/skyflow/vault/controller/ConnectionController.java b/src/main/java/com/skyflow/vault/controller/ConnectionController.java index 4a9334d4..07f12dc8 100644 --- a/src/main/java/com/skyflow/vault/controller/ConnectionController.java +++ b/src/main/java/com/skyflow/vault/controller/ConnectionController.java @@ -55,16 +55,37 @@ public InvokeConnectionResponse invoke(InvokeConnectionRequest invokeConnectionR Object requestBodyObject = invokeConnectionRequest.getRequestBody(); if (requestBodyObject != null) { - try { - requestBody = convertObjectToJson(requestBodyObject); - } catch (Exception e) { - LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); - throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + if (requestBodyObject instanceof String) { + String contentType = headers.getOrDefault("content-type", ""); + if (!contentType.isEmpty() && !contentType.toLowerCase().contains("application/json")) { + requestBody = new JsonObject(); + requestBody.addProperty(Constants.HttpUtilityExtra.RAW_BODY_KEY, (String) requestBodyObject); + } else { + try { + requestBody = convertObjectToJson(requestBodyObject); + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + } + } + } else { + try { + requestBody = convertObjectToJson(requestBodyObject); + } catch (Exception e) { + LogUtil.printErrorLog(ErrorLogs.INVALID_REQUEST_HEADERS.getLog()); + throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), ErrorMessage.InvalidRequestBody.getMessage()); + } } } String response = HttpUtility.sendRequest(requestMethod.name(), new URL(filledURL), requestBody, headers); - JsonObject data = JsonParser.parseString(response).getAsJsonObject(); + JsonObject data; + try { + data = JsonParser.parseString(response).getAsJsonObject(); + } catch (Exception e) { + data = new JsonObject(); + data.addProperty("response", response); + } HashMap metadata = new HashMap<>(); metadata.put("requestId", HttpUtility.getRequestID()); connectionResponse = new InvokeConnectionResponse(data, metadata, null); diff --git a/src/test/java/com/skyflow/utils/HttpUtilityTests.java b/src/test/java/com/skyflow/utils/HttpUtilityTests.java index f7214690..2861ed9c 100644 --- a/src/test/java/com/skyflow/utils/HttpUtilityTests.java +++ b/src/test/java/com/skyflow/utils/HttpUtilityTests.java @@ -124,4 +124,69 @@ public void testSendRequestError() { fail(INVALID_EXCEPTION_THROWN); } } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithRawBody() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/xml"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/xml"); + JsonObject params = new JsonObject(); + params.addProperty("__raw_body__", "test"); + String response = httpUtility.sendRequest("POST", url, params, headers); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithoutContentTypeHeader() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/json"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value"); + String response = httpUtility.sendRequest("POST", url, params, null); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestWithNullRequestId() { + try { + given(mockConnection.getHeaderField(anyString())).willReturn(null); + given(mockConnection.getRequestProperty("content-type")).willReturn("application/json"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/json"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value"); + String response = httpUtility.sendRequest("GET", url, params, headers); + Assert.assertEquals(expected, response); + Assert.assertNotNull(HttpUtility.getRequestID()); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } + + @Test + @PrepareForTest({URL.class, HttpURLConnection.class}) + public void testSendRequestFormURLEncodedWithSpecialCharacters() { + try { + given(mockConnection.getRequestProperty("content-type")).willReturn("application/x-www-form-urlencoded"); + Map headers = new HashMap<>(); + headers.put("content-type", "application/x-www-form-urlencoded"); + JsonObject params = new JsonObject(); + params.addProperty("key", "value with spaces"); + params.addProperty("special", "test@email.com"); + String response = httpUtility.sendRequest("POST", url, params, headers); + Assert.assertEquals(expected, response); + } catch (Exception e) { + fail(INVALID_EXCEPTION_THROWN); + } + } } From 033c061bff33c940091b7589ed151c2852b3fa79 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Tue, 19 May 2026 16:22:39 +0530 Subject: [PATCH 48/48] fix: guard unsafe Optional.get() calls and remove dead statement in DetectController - Remove dead statement `response.getEntities().get(0).getFile()` that discarded its result and threw IndexOutOfBoundsException/NPE when entities was null or empty (C1); the guarded block below already handles entity processing correctly - Replace unguarded `response.getOutput().get()` in getFirstOutput() with `.orElse(null)` so an absent output Optional returns null instead of throwing NoSuchElementException (C2) - Replace unguarded `firstOutput.getProcessedFileExtension().get().toString()` with `.map(Object::toString).orElse(UNKNOWN)`, reusing the already-computed Optional and matching the safe pattern used for processedFileType one line above (C3) - Add 4 unit tests covering all three fixes via reflection Co-Authored-By: Claude Sonnet 4.6 --- .../vault/controller/DetectController.java | 7 ++- .../controller/DetectControllerFileTests.java | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/skyflow/vault/controller/DetectController.java b/src/main/java/com/skyflow/vault/controller/DetectController.java index d2dd891d..873c83e1 100644 --- a/src/main/java/com/skyflow/vault/controller/DetectController.java +++ b/src/main/java/com/skyflow/vault/controller/DetectController.java @@ -135,7 +135,6 @@ public DeidentifyFileResponse deidentifyFile(DeidentifyFileRequest request) thro if (DeidentifyFileStatus.SUCCESS.value().equalsIgnoreCase(response.getStatus())) { String base64File = response.getFileBase64(); - response.getEntities().get(0).getFile(); if (base64File != null) { byte[] decodedBytes = Base64.getDecoder().decode(base64File); String outputDir = request.getOutputDirectory(); @@ -297,7 +296,9 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D .map(Object::toString) .orElse(DetectRunsResponseStatus.UNKNOWN.toString()); - String fileExtension = firstOutput.getProcessedFileExtension().get().toString(); + String fileExtension = processedFileExtension + .map(Object::toString) + .orElse(DetectRunsResponseStatus.UNKNOWN.toString()); Float sizeInKb = response.getSize().orElse(null); Float durationInSeconds = response.getDuration().orElse(null); DeidentifyFileResponse deidentifyFileResponse = new DeidentifyFileResponse( @@ -320,7 +321,7 @@ private static synchronized DeidentifyFileResponse parseDeidentifyFileResponse(D } private static synchronized DeidentifiedFileOutput getFirstOutput(DetectRunsResponse response) { - List outputs = response.getOutput().get(); + List outputs = response.getOutput().orElse(null); return outputs != null && !outputs.isEmpty() ? outputs.get(0) : null; } diff --git a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java index da8494e1..2b8379bc 100644 --- a/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java +++ b/src/test/java/com/skyflow/vault/controller/DetectControllerFileTests.java @@ -5,8 +5,12 @@ import com.skyflow.errors.ErrorCode; import com.skyflow.errors.ErrorMessage; import com.skyflow.errors.SkyflowException; +import com.skyflow.generated.rest.types.DeidentifiedFileOutput; +import com.skyflow.generated.rest.types.DetectRunsResponse; +import com.skyflow.generated.rest.types.DetectRunsResponseStatus; import com.skyflow.vault.detect.AudioBleep; import com.skyflow.vault.detect.DeidentifyFileRequest; +import com.skyflow.vault.detect.DeidentifyFileResponse; import com.skyflow.vault.detect.FileInput; import com.skyflow.vault.detect.GetDetectRunRequest; import org.junit.Assert; @@ -14,8 +18,10 @@ import org.junit.Test; import java.io.File; +import java.lang.reflect.Method; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; public class DetectControllerFileTests { private static final String EXCEPTION_NOT_THROWN = "Should have thrown an exception"; @@ -406,4 +412,54 @@ public void testOutputDirectoryNotWritable() throws Exception { dir.delete(); } } + + // C1 regression: the dead statement response.getEntities().get(0) has been removed. + // The short-form constructor produces null entities; the guarded block at the call site + // handles that safely. + @Test + public void testDeidentifyFileResponseNullEntitiesDoesNotThrow() { + DeidentifyFileResponse response = new DeidentifyFileResponse("run-id", "IN_PROGRESS"); + Assert.assertNull("Entities must be null from short-form constructor", response.getEntities()); + } + + // C2: getFirstOutput must return null (not throw NoSuchElementException) when output is absent. + @Test + public void testGetFirstOutputReturnsNullWhenOutputAbsent() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder().build(); + Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class); + method.setAccessible(true); + DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response); + Assert.assertNull("getFirstOutput must return null when output Optional is absent", result); + } + + // C2: getFirstOutput must return null when output list is present but empty. + @Test + public void testGetFirstOutputReturnsNullWhenOutputListEmpty() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder() + .output(Collections.emptyList()) + .build(); + Method method = DetectController.class.getDeclaredMethod("getFirstOutput", DetectRunsResponse.class); + method.setAccessible(true); + DeidentifiedFileOutput result = (DeidentifiedFileOutput) method.invoke(null, response); + Assert.assertNull("getFirstOutput must return null when output list is empty", result); + } + + // C3: parseDeidentifyFileResponse must not throw when processedFileExtension is absent; + // it should fall back to the UNKNOWN sentinel value. + @Test + public void testParseDeidentifyFileResponseFallsBackWhenExtensionAbsent() throws Exception { + DetectRunsResponse response = DetectRunsResponse.builder() + .status(DetectRunsResponseStatus.SUCCESS) + .output(Collections.singletonList(DeidentifiedFileOutput.builder().build())) + .build(); + + Method method = DetectController.class.getDeclaredMethod( + "parseDeidentifyFileResponse", DetectRunsResponse.class, String.class, String.class); + method.setAccessible(true); + DeidentifyFileResponse result = (DeidentifyFileResponse) method.invoke(null, response, "run-id", "SUCCESS"); + + Assert.assertNotNull("Response must not be null", result); + Assert.assertEquals("Extension must fall back to UNKNOWN when absent", + DetectRunsResponseStatus.UNKNOWN.toString(), result.getExtension()); + } } \ No newline at end of file