Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
e941688
feat(schema): enable searchableJson() method for SteVec indexing
tobyhede Jan 15, 2026
6a9208c
feat(protect): add JSON search term types for containment and path qu…
tobyhede Jan 15, 2026
9558fc2
feat(protect): add query encryption operations with comprehensive tests
tobyhede Jan 16, 2026
13ab8d3
refactor(protect): remove unintended public query API
tobyhede Jan 16, 2026
a112a44
chore: update protect-ffi to 0.20.0
tobyhede Jan 19, 2026
bedda29
fix: use local type definitions until protect-ffi 0.20.0 release
tobyhede Jan 19, 2026
b0c00d2
chore: update protect-ffi to 0.20.0
tobyhede Jan 19, 2026
795e082
test(protect): add comprehensive JSON search terms tests
tobyhede Jan 19, 2026
e4bc7a6
feat(protect): expose JSON and query search operations via public API
tobyhede Jan 19, 2026
b660611
docs: add documentation for searchable encrypted JSON
tobyhede Jan 19, 2026
f5e4793
test(protect): add comprehensive tests for explicit query encryption …
tobyhede Jan 19, 2026
def4f0d
feat(protect): extend SearchTerm type to support JSON search terms
tobyhede Jan 19, 2026
3c82c71
feat(protect): implement JSON support in SearchTermsOperation
tobyhede Jan 19, 2026
6be18b4
test(protect): add JSON support tests for createSearchTerms
tobyhede Jan 19, 2026
2d7a40d
deprecate(protect): mark createJsonSearchTerms as deprecated
tobyhede Jan 19, 2026
b6f3fd3
refactor(protect): remove deprecated createJsonSearchTerms API
tobyhede Jan 20, 2026
3f9aed2
test(protect): add lock context tests and optimize client initialization
tobyhede Jan 20, 2026
8596d2e
refactor(schema): replace magic string with ste_vec prefix inference
tobyhede Jan 20, 2026
8be7455
test(protect): add missing test coverage for edge cases
tobyhede Jan 20, 2026
6854834
fix(schema): resolve ste_vec prefix type mismatch in DTS build
tobyhede Jan 20, 2026
3181da1
docs: address PR #257 code review feedback for searchable JSON API
tobyhede Jan 21, 2026
c0d66df
refactor(docs): address code review suggestions
tobyhede Jan 21, 2026
6a90fcd
feat(types): add QueryTerm union types for unified encryptQuery API
tobyhede Jan 21, 2026
1523dec
feat(types): add type guards for QueryTerm variants
tobyhede Jan 21, 2026
5357cb6
feat(exports): export QueryTerm types and type guards
tobyhede Jan 21, 2026
2415c4d
feat(operations): add BatchEncryptQueryOperation for batch encryptQuery
tobyhede Jan 21, 2026
30e6cfc
feat(encryptQuery): add batch overload for array of QueryTerms
tobyhede Jan 21, 2026
c888923
test(encryptQuery): add comprehensive batch tests for JSON and mixed …
tobyhede Jan 21, 2026
6700f45
deprecate(createQuerySearchTerms): mark as deprecated in favor of enc…
tobyhede Jan 21, 2026
7b4a95f
deprecate(createSearchTerms): mark as deprecated in favor of encryptQ…
tobyhede Jan 21, 2026
e311d32
fix(types): resolve DTS build error in encryptQuery overload type nar…
tobyhede Jan 21, 2026
354818a
style: fix linting issues in batch-encrypt-query and related files
tobyhede Jan 21, 2026
dbcc596
docs: update all documentation to use unified encryptQuery API
tobyhede Jan 21, 2026
4775db2
test(encryptQuery): add withLockContext test for batch operations
tobyhede Jan 21, 2026
37d6d60
refactor(encryptQuery): extract isQueryTermArray type guard for clean…
tobyhede Jan 21, 2026
97b2270
docs: sync documentation with encryptQuery unified API implementation
tobyhede Jan 21, 2026
14301bf
fix(encryptQuery): handle empty array input correctly
tobyhede Jan 21, 2026
e389d3f
feat(encryptQuery): make indexType optional with auto-inference support
tobyhede Jan 21, 2026
f32cc1d
fix(encryptQuery): correct docs and export missing types
tobyhede Jan 21, 2026
c6bc59b
chore: merge
calvinbrewer Jan 21, 2026
50d5f27
refactor(encryptQuery): rename indexType to queryType with schema-mat…
tobyhede Jan 22, 2026
70b1ea1
refactor(drizzle): export ColumnInfo interface
tobyhede Jan 22, 2026
cb6fc24
feat(drizzle): add searchableJson to EncryptedColumnConfig
tobyhede Jan 22, 2026
6893db0
feat(drizzle): add normalizePath helper for JSON path formats
tobyhede Jan 22, 2026
5bc666b
feat(drizzle): extract searchableJson config to ProtectColumn
tobyhede Jan 22, 2026
0f958bc
feat(drizzle): add JsonPathBuilder class skeleton
tobyhede Jan 22, 2026
7d9e2ba
feat(drizzle): add LazyJsonOperator interface and type guard
tobyhede Jan 22, 2026
4684008
feat(drizzle): add eq() method to JsonPathBuilder
tobyhede Jan 22, 2026
7e80e58
feat(drizzle): add ne, contains, containedBy to JsonPathBuilder
tobyhede Jan 22, 2026
e6e78ac
feat(drizzle): add selector-based path extraction to JsonPathBuilder
tobyhede Jan 22, 2026
ce41d25
feat(drizzle): add get() and arrayLength() to JsonPathBuilder
tobyhede Jan 22, 2026
8cbbf89
feat(drizzle): add jsonPath() to createProtectOperators
tobyhede Jan 22, 2026
d515cd9
feat(drizzle): add elements() and elementsText() to JsonPathBuilder
tobyhede Jan 22, 2026
1e5ba7e
feat(drizzle): export JSON operator utilities from package
tobyhede Jan 22, 2026
28c04e4
feat(drizzle): add JSON operator batching to and()
tobyhede Jan 22, 2026
566b954
test(drizzle): add test for and() with JSON operators
tobyhede Jan 22, 2026
b98b0d3
feat(drizzle): add JSON operator batching to or()
tobyhede Jan 22, 2026
42c3610
feat(drizzle): implement execute() for LazyJsonOperator
tobyhede Jan 22, 2026
0fd9f93
docs(drizzle): add comprehensive JSDoc to jsonPath()
tobyhede Jan 22, 2026
09675c9
fix(drizzle): add proper mocks for and()/or() JSON operator tests
tobyhede Jan 22, 2026
cf28220
fix: query operator interface
calvinbrewer Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/searchable-json-query-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cipherstash/protect": major
"@cipherstash/schema": major
---

Add searchable JSON query API with path and containment query support
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,10 @@ mise.local.toml
cipherstash.toml
cipherstash.secret.toml
sql/cipherstash-*.sql

# work files
.claude/
.serena/
.work/
**/.work/
PR_REVIEW.md
7 changes: 4 additions & 3 deletions docs/concepts/aws-kms-vs-cipherstash-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,12 @@ const encryptResult = await protectClient.encrypt(
);

// Create search terms and query directly in PostgreSQL
const searchTerms = await protectClient.createSearchTerms({
terms: ['secret'],
const searchTerms = await protectClient.encryptQuery([{
value: 'secret',
column: users.email,
table: users,
});
queryType: queryTypes.freeTextSearch,
}]);

// Use with your ORM (Drizzle integration included)
```
Expand Down
11 changes: 6 additions & 5 deletions docs/concepts/searchable-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ CipherStash uses [EQL](https://github.com/cipherstash/encrypt-query-language) to
// 1) Encrypt the search term
const searchTerm = 'alice.johnson@example.com'

const encryptedParam = await protectClient.createSearchTerms([{
const encryptedParam = await protectClient.encryptQuery([{
value: searchTerm,
table: protectedUsers, // Reference to the Protect table schema
column: protectedUsers.email, // Your Protect column definition
queryType: queryTypes.equality, // Use 'equality' for exact match queries
}])

if (encryptedParam.failure) {
// Handle the failure
throw new Error(encryptedParam.failure.message)
}

// 2) Build an equality query noting that EQL must be installed in order for the operation to work successfully
Expand All @@ -86,10 +88,9 @@ const equalitySQL = `
WHERE email = $1
`

// 3) Execute the query, passing in the Postgres column name
// and the encrypted search term as the second parameter
// 3) Execute the query, passing in the encrypted search term
// (client is an arbitrary Postgres client)
const result = await client.query(equalitySQL, [ protectedUser.email.getName(), encryptedParam.data ])
const result = await client.query(equalitySQL, [encryptedParam.data[0]])
```

Using the above approach, Protect.js is generating the EQL payloads and which means you never have to drop down to writing complex SQL queries.
Expand Down Expand Up @@ -132,7 +133,7 @@ With searchable encryption, you can:
With searchable encryption:

- Data can be encrypted, stored, and searched in your existing PostgreSQL database.
- Encrypted data can be searched using equality, free text search, and range queries.
- Encrypted data can be searched using equality, free text search, range queries, and JSON path/containment queries.
- Data remains encrypted, and will be decrypted using the Protect.js library in your application.
- Queries are blazing fast, and won't slow down your application experience.
- Every decryption event is logged, giving you an audit trail of data access events.
Expand Down
8 changes: 8 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ CREATE TABLE users (
);
```

## Next steps

Now that you have the basics working, explore these advanced features:

- **[Searchable Encryption](./reference/searchable-encryption-postgres.md)** - Learn how to search encrypted data using `encryptQuery()` with PostgreSQL and EQL
- **[Model Operations](./reference/model-operations.md)** - Encrypt and decrypt entire objects with bulk operations
- **[Schema Configuration](./reference/schema.md)** - Configure indexes for equality, free text search, range queries, and JSON search

---

### Didn't find what you wanted?
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/model-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For better performance when working with multiple models, use these bulk encrypt
### Bulk encryption

```typescript
const users = [
const usersList = [
{
id: "1",
email: "user1@example.com",
Expand All @@ -88,7 +88,7 @@ const users = [
},
];

const encryptedResult = await protectClient.bulkEncryptModels(users, users);
const encryptedResult = await protectClient.bulkEncryptModels(usersList, usersSchema);

if (encryptedResult.failure) {
console.error("Bulk encryption failed:", encryptedResult.failure.message);
Expand Down
34 changes: 29 additions & 5 deletions docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,29 @@ export const protectedUsers = csTable("users", {
});
```

### Searchable JSON

To enable searching within JSON columns, use the `searchableJson()` method. This automatically sets the column data type to `json` and configures the necessary indexes for path and containment queries.

```ts
import { csTable, csColumn } from "@cipherstash/protect";

export const protectedUsers = csTable("users", {
metadata: csColumn("metadata").searchableJson(),
});
```

> [!WARNING]
> `searchableJson()` is mutually exclusive with other index types (`equality()`, `freeTextSearch()`, `orderAndRange()`) on the same column. Combining them will result in runtime errors. This is enforced by the encryption backend, not at the TypeScript type level.


### Nested objects

Protect.js supports nested objects in your schema, allowing you to encrypt **but not search on** nested properties. You can define nested objects up to 3 levels deep.
Protect.js supports nested objects in your schema, allowing you to encrypt nested properties. You can define nested objects up to 3 levels deep using `csValue`. For **searchable** JSON data, use `.searchableJson()` on a JSON column instead.

> [!TIP]
> If you need to search within JSON data, use `.searchableJson()` on the column instead of nested `csValue` definitions. See [Searchable JSON](#searchable-json) above.

This is useful for data stores that have less structured data, like NoSQL databases.

You can define nested objects by using the `csValue` function to define a value in a nested object. The value naming convention of the `csValue` function is a dot-separated string of the nested object path, e.g. `profile.name` or `profile.address.street`.
Expand All @@ -105,15 +125,15 @@ export const protectedUsers = csTable("users", {
```

When working with nested objects:
- Searchable encryption is not supported on nested objects
- Searchable encryption is not supported on nested `csValue` objects (use `.searchableJson()` for searchable JSON)
- Each level can have its own encrypted fields
- The maximum nesting depth is 3 levels
- Null and undefined values are supported at any level
- Optional nested objects are supported

> [!WARNING]
> TODO: The schema builder does not validate the values you supply to the `csValue` or `csColumn` functions.
> These values are meant to be unique, and and cause unexpected behavior if they are not defined correctly.
> The schema builder does not currently validate the values you supply to the `csValue` or `csColumn` functions.
> These values must be unique within your schema - duplicate values may cause unexpected behavior.

## Available index options

Expand All @@ -124,8 +144,12 @@ The following index options are available for your schema:
| equality | Enables a exact index for equality queries. | `WHERE email = 'example@example.com'` |
| freeTextSearch | Enables a match index for free text queries. | `WHERE description LIKE '%example%'` |
| orderAndRange | Enables an sorting and range queries index. | `ORDER BY price ASC` |
| searchableJson | Enables searching inside JSON columns. | `WHERE data->'user'->>'email' = '...'` |

You can chain these methods to your column to configure them in any combination.
You can chain `equality()`, `freeTextSearch()`, and `orderAndRange()` methods in any combination.

> [!WARNING]
> `searchableJson()` is **mutually exclusive** with other index types. Do not combine `searchableJson()` with `equality()`, `freeTextSearch()`, or `orderAndRange()` on the same column.

## Initializing the Protect client

Expand Down
Loading