Skip to content

feat(stack): usability improvements#301

Merged
calvinbrewer merged 6 commits intomainfrom
usability-1
Feb 25, 2026
Merged

feat(stack): usability improvements#301
calvinbrewer merged 6 commits intomainfrom
usability-1

Conversation

@calvinbrewer
Copy link
Contributor

@calvinbrewer calvinbrewer commented Feb 24, 2026

Summary by CodeRabbit

  • Type Safety

    • encrypt/encryptQuery, query/bulk operations, and related types now require non-null plaintexts.
  • New Features

    • Added a builder to aggregate encrypt configuration definitions.
    • Default column cast changed from "string" to "text" in schema builders.
  • Documentation

    • Docs and examples updated to remove null-plaintext patterns and show non-null requirements and cast type change.
  • Tests

    • Removed tests covering null-plaintext scenarios across encryption and query flows.

@changeset-bot
Copy link

changeset-bot bot commented Feb 24, 2026

🦋 Changeset detected

Latest commit: c447e26

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@cipherstash/stack Minor
@cipherstash/basic-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02249de and c447e26.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • packages/stack/src/schema/index.ts

📝 Walkthrough

Walkthrough

This PR removes null plaintext support across the stack package: types, public API signatures, operation implementations, helpers, docs/examples, and tests. Batch/bulk paths now process all items uniformly (no null-short-circuits) and encryption/decryption types and results are non-nullable.

Changes

Cohort / File(s) Summary
Release Metadata
​.changeset/fresh-phones-smile.md
Added a changeset documenting the minor version bump and removal of null plaintext support.
Public API & Types
packages/stack/src/encryption/index.ts, packages/stack/src/types.ts, packages/stack/src/types-public.ts
Removed null from plaintext-related method signatures and public types (removed EncryptPayload); adjusted conditional type mappings so encrypted/decrypted types are non-nullable except where schema-nullability is explicit.
Core Operations
packages/stack/src/encryption/operations/encrypt.ts, packages/stack/src/encryption/operations/encrypt-query.ts, packages/stack/src/encryption/operations/decrypt.ts
Made plaintext mandatory, removed early-return/null-guard branches, updated constructors and generics to non-null plaintext, and pass plaintext directly to FFI calls.
Batch / Bulk Ops
packages/stack/src/encryption/operations/batch-encrypt-query.ts, packages/stack/src/encryption/operations/bulk-encrypt.ts, packages/stack/src/encryption/operations/bulk-decrypt.ts
Dropped null-filtering helpers and all-null short-circuits; payload construction always includes items; result assembly simplified to map encrypted/decrypted outputs directly to terms/order.
Helpers
packages/stack/src/encryption/helpers/index.ts
Removed runtime null guards from composite formatting helpers; null inputs no longer throw there.
Schema / Builders
packages/stack/src/schema/index.ts, packages/stack/__tests__/schema-builders.test.ts
Added text cast option, changed default cast from stringtext, normalised 'string' to 'text', and added buildEncryptConfig builder; tests updated accordingly.
Tests & Examples
packages/stack/__tests__/* (many), packages/stack/README.md, skills/stash-encryption/SKILL.md
Removed numerous tests and examples that covered null plaintext handling across suites; examples updated to use non-null plaintexts.
Docs
docs/reference/model-operations.md, docs/reference/searchable-encryption-postgres.md
Removed claims about automatic null handling and adjusted searchableJson/encryptQuery docs to reflect non-nullable results.
Dependency bump
packages/stack/package.json
Bumped @cipherstash/protect-ffi from 0.20.1 → 0.20.2.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tobyhede
  • coderdan
  • CDThomas

Poem

🐰 I nibbled nulls from code and pranced away,
Every plaintext now must brighten the day.
Tests slimmed like carrots, types trimmed and neat,
Batch flows march forward, no empty retreat.
🥕 Hop-hop — a rabbit's tiny codebeat.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'feat(stack): usability improvements' is vague and overly broad; it does not clearly describe the main change of removing null from Encrypted types and APIs. Consider a more specific title such as 'feat(stack): remove null from Encrypted type and encryption APIs' to clearly convey the primary change.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch usability-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack/src/encryption/operations/bulk-encrypt.ts`:
- Around line 94-97: Fix the positional mapping in BulkEncryptOperation.execute
and BulkEncryptOperationWithLockContext.execute: before calling encryptBulk,
create a filtered array of non-null plaintexts (preserving order), call
encryptBulk on that, then rebuild the returned results by iterating over the
original this.plaintexts and using a counter into the encryptedData array to
assign data for non-null entries and { id, data: null } for null plaintexts so
ids remain correctly paired with their encrypted payloads.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa2b85d and 5c3f4e7.

📒 Files selected for processing (20)
  • .changeset/fresh-phones-smile.md
  • packages/stack/__tests__/bulk-protect.test.ts
  • packages/stack/__tests__/encrypt-query-searchable-json.test.ts
  • packages/stack/__tests__/encrypt-query-stevec.test.ts
  • packages/stack/__tests__/encrypt-query.test.ts
  • packages/stack/__tests__/encryption-helpers.test.ts
  • packages/stack/__tests__/json-protect.test.ts
  • packages/stack/__tests__/number-protect.test.ts
  • packages/stack/__tests__/protect-ops.test.ts
  • packages/stack/__tests__/searchable-json-pg.test.ts
  • packages/stack/src/encryption/helpers/index.ts
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/encryption/operations/batch-encrypt-query.ts
  • packages/stack/src/encryption/operations/bulk-decrypt.ts
  • packages/stack/src/encryption/operations/bulk-encrypt.ts
  • packages/stack/src/encryption/operations/decrypt.ts
  • packages/stack/src/encryption/operations/encrypt-query.ts
  • packages/stack/src/encryption/operations/encrypt.ts
  • packages/stack/src/types-public.ts
  • packages/stack/src/types.ts
💤 Files with no reviewable changes (9)
  • packages/stack/tests/searchable-json-pg.test.ts
  • packages/stack/src/types-public.ts
  • packages/stack/tests/protect-ops.test.ts
  • packages/stack/tests/encrypt-query-stevec.test.ts
  • packages/stack/tests/encrypt-query.test.ts
  • packages/stack/src/encryption/helpers/index.ts
  • packages/stack/tests/number-protect.test.ts
  • packages/stack/tests/encryption-helpers.test.ts
  • packages/stack/tests/json-protect.test.ts

Comment on lines +94 to +97
return encryptedData.map((data, i) => ({
id: this.plaintexts[i].id,
data,
}))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -path "*/protect-ffi/*" | head -20

Repository: cipherstash/protectjs

Length of output: 47


🏁 Script executed:

git ls-files | grep -E "(protect-ffi|bulk-encrypt)" | head -20

Repository: cipherstash/protectjs

Length of output: 422


🏁 Script executed:

cat -n packages/stack/src/encryption/operations/bulk-encrypt.ts | head -100

Repository: cipherstash/protectjs

Length of output: 3581


🏁 Script executed:

rg -n "encryptBulk" --type=ts -A 5 -B 2 | head -100

Repository: cipherstash/protectjs

Length of output: 8131


🏁 Script executed:

cat -n packages/protect/src/ffi/operations/bulk-encrypt.ts | head -150

Repository: cipherstash/protectjs

Length of output: 5184


🏁 Script executed:

rg -n "originalIndex" --type=ts

Repository: cipherstash/protectjs

Length of output: 2014


🏁 Script executed:

fd -t f -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "encryptBulk" | head -10

Repository: cipherstash/protectjs

Length of output: 295


🏁 Script executed:

rg -n "encryptBulk" packages/protect/src/ffi/operations/bulk-encrypt.ts -A 10 -B 2 | head -150

Repository: cipherstash/protectjs

Length of output: 1514


🏁 Script executed:

cat packages/stack/src/encryption/operations/bulk-encrypt.ts | sed -n '23,97p'

Repository: cipherstash/protectjs

Length of output: 2068


🏁 Script executed:

rg -n "plaintext.*null" packages/protect/src/ffi/operations/ --type=ts -B 2 -A 2 | head -80

Repository: cipherstash/protectjs

Length of output: 5099


🏁 Script executed:

cat packages/protect/src/ffi/operations/bulk-encrypt.ts | sed -n '49,69p'

Repository: cipherstash/protectjs

Length of output: 595


🏁 Script executed:

cat packages/stack/src/encryption/operations/bulk-encrypt.ts | sed -n '160,185p'

Repository: cipherstash/protectjs

Length of output: 746


🏁 Script executed:

rg -n "class BulkEncryptOperationWithLockContext" packages/stack/src/encryption/operations/bulk-encrypt.ts -A 40 | head -50

Repository: cipherstash/protectjs

Length of output: 1466


Apply null-safe result mapping to both BulkEncryptOperation.execute() and BulkEncryptOperationWithLockContext.execute().

The current positional mapping assumes encryptBulk returns exactly one result per input in the same order:

encryptedData.map((data, i) => ({
  id: this.plaintexts[i].id,
  data,
}))

However, the FFI layer filters null plaintexts before encryption (see packages/protect/src/ffi/operations/bulk-encrypt.ts lines 30–40 and 49–69), so the encrypted results array will have fewer items than the input array if any plaintexts are null. This causes a silent mismatch: this.plaintexts[i].id will resolve to the wrong plaintext when i exceeds the count of non-null items.

Implement the same pattern used in the FFI layer: filter nulls before encryption, then reconstruct the full result array using a counter to track the position in the filtered encrypted data. This ensures null plaintexts map to { id, data: null } without corrupting the id-to-data mapping.

Affects both variants: lines 94–97 and 182–185.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack/src/encryption/operations/bulk-encrypt.ts` around lines 94 -
97, Fix the positional mapping in BulkEncryptOperation.execute and
BulkEncryptOperationWithLockContext.execute: before calling encryptBulk, create
a filtered array of non-null plaintexts (preserving order), call encryptBulk on
that, then rebuild the returned results by iterating over the original
this.plaintexts and using a counter into the encryptedData array to assign data
for non-null entries and { id, data: null } for null plaintexts so ids remain
correctly paired with their encrypted payloads.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/stack/src/schema/index.ts (1)

148-157: ⚠️ Potential issue | 🟡 Minor

JSDoc for EncryptedField.dataType() still documents the default as 'string'; effective default is now 'text'.

Lines 153–156 say "By default all values are treated as 'string'". The same staleness exists in EncryptedColumn.dataType() at lines 200–204.

📝 Proposed doc fix
-  * By default all values are treated as `'string'`. Use this method to specify
-  * a different type so the encryption layer knows how to encode the plaintext
-  * before encrypting.
+  * By default all values are treated as `'text'`. Use this method to specify
+  * a different type so the encryption layer knows how to encode the plaintext
+  * before encrypting.

Apply the same fix to the identical paragraph in EncryptedColumn.dataType() (around line 200–204).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack/src/schema/index.ts` around lines 148 - 157, Update the JSDoc
for EncryptedField.dataType() and EncryptedColumn.dataType(): change the
sentence that currently states "By default all values are treated as 'string'."
to reflect the effective default "text" (e.g., "By default all values are
treated as 'text'.") so the docs match the implementation and keep the rest of
the description intact.
packages/stack/README.md (1)

627-631: ⚠️ Potential issue | 🟡 Minor

Update stale csValue reference to encryptedField in README.

Lines 627–631 incorrectly reference csValue(valueName) (returning ProtectValue), which is not exported from the current API. The correct export is encryptedField(valueName) returning EncryptedField. The same stale name appears in the Subpath Exports table at line 639.

Proposed fixes

Lines 627–631:

-csValue(valueName)                 // returns ProtectValue (for nested values)
+encryptedField(valueName)          // returns EncryptedField (for nested encrypted fields)

Subpath Exports table:

-| `@cipherstash/stack/schema` | `encryptedTable`, `encryptedColumn`, `csValue`, schema types |
+| `@cipherstash/stack/schema` | `encryptedTable`, `encryptedColumn`, `encryptedField`, schema types |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack/README.md` around lines 627 - 631, The README contains stale
API names: replace any occurrences of csValue(valueName) and its return type
ProtectValue with the current API encryptedField(valueName) returning
EncryptedField (e.g., in the example block alongside encryptedTable and
encryptedColumn and in the Subpath Exports table); update the example line to
use encryptedField(valueName) and adjust the return type text to EncryptedField
so the docs match the exported symbols.
🧹 Nitpick comments (1)
packages/stack/src/schema/index.ts (1)

192-195: EncryptedColumn constructor initializes castAsValue to 'string' while EncryptedField was updated to 'text'.

The inconsistency is masked by the normalization in build(), but leaves internal state out of sync between the two classes. Worth aligning.

♻️ Proposed fix
  constructor(columnName: string) {
    this.columnName = columnName
-   this.castAsValue = 'string'
+   this.castAsValue = 'text'
  }

With this change the guard in build() at line 341 (=== 'string' ? 'text' : ...) becomes a no-op safety net, which is fine to leave in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack/src/schema/index.ts` around lines 192 - 195, The
EncryptedColumn constructor sets this.castAsValue = 'string' which drifts from
EncryptedField's updated default of 'text'; update the EncryptedColumn
constructor (the constructor in class EncryptedColumn) to initialize castAsValue
to 'text' so internal state matches EncryptedField, leaving the existing guard
in build() (the === 'string' ? 'text' : ...) as a redundant safety net.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack/__tests__/schema-builders.test.ts`:
- Around line 23-31: Update the test descriptions to match the actual asserted
value: rename the test titles that currently say "string" to say "text" so they
reflect the expectation of built.cast_as === 'text'; specifically modify the
tests around encryptedColumn('name').build()/dataType('string') (using
encryptedColumn, .dataType, and .build which assert cast_as) and the other
occurrence at the later test (around line ~317) to change "defaults castAs to
string" and ".dataType(\"string\") sets castAs to string" to use "text" instead
of "string".

---

Outside diff comments:
In `@packages/stack/README.md`:
- Around line 627-631: The README contains stale API names: replace any
occurrences of csValue(valueName) and its return type ProtectValue with the
current API encryptedField(valueName) returning EncryptedField (e.g., in the
example block alongside encryptedTable and encryptedColumn and in the Subpath
Exports table); update the example line to use encryptedField(valueName) and
adjust the return type text to EncryptedField so the docs match the exported
symbols.

In `@packages/stack/src/schema/index.ts`:
- Around line 148-157: Update the JSDoc for EncryptedField.dataType() and
EncryptedColumn.dataType(): change the sentence that currently states "By
default all values are treated as 'string'." to reflect the effective default
"text" (e.g., "By default all values are treated as 'text'.") so the docs match
the implementation and keep the rest of the description intact.

---

Nitpick comments:
In `@packages/stack/src/schema/index.ts`:
- Around line 192-195: The EncryptedColumn constructor sets this.castAsValue =
'string' which drifts from EncryptedField's updated default of 'text'; update
the EncryptedColumn constructor (the constructor in class EncryptedColumn) to
initialize castAsValue to 'text' so internal state matches EncryptedField,
leaving the existing guard in build() (the === 'string' ? 'text' : ...) as a
redundant safety net.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c3f4e7 and 02249de.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • docs/reference/model-operations.md
  • docs/reference/searchable-encryption-postgres.md
  • packages/stack/README.md
  • packages/stack/__tests__/schema-builders.test.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/schema/index.ts
  • skills/stash-encryption/SKILL.md
💤 Files with no reviewable changes (1)
  • docs/reference/model-operations.md
✅ Files skipped from review due to trivial changes (1)
  • packages/stack/package.json

@calvinbrewer calvinbrewer changed the title feat(stack): remove null from Encrypted type feat(stack): usability improvements Feb 24, 2026
@calvinbrewer calvinbrewer merged commit 8bfe400 into main Feb 25, 2026
1 of 2 checks passed
@calvinbrewer calvinbrewer deleted the usability-1 branch February 25, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants