Skip to content

Commit b130505

Browse files
authored
Merge pull request #287 from cipherstash/drizzle-searchable-jsonb
Add JSONB query operators for drizzle: jsonbPathQueryFirst, jsonbGet, and jsonbPathExists
2 parents ee8eff8 + e3e888c commit b130505

16 files changed

+1753
-468
lines changed

docs/reference/drizzle/drizzle.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,56 @@ return results
274274

275275
---
276276

277+
## JSONB queries with encrypted data
278+
279+
Protect.js supports querying encrypted JSON columns using JSONB operators. These operators require `searchableJson: true` and `dataType: 'json'` in the column's `encryptedType` config.
280+
281+
> [!TIP]
282+
> For details on the low-level `encryptQuery` API and JSONB query types (`steVecSelector`, `steVecTerm`), see the [Searchable encryption with PostgreSQL](../searchable-encryption-postgres.md#jsonb-queries-with-searchablejson-recommended) reference.
283+
284+
### Check path existence with `jsonbPathExists`
285+
286+
Use `jsonbPathExists` to check if a JSONPath exists in the JSONB data. This returns a boolean and can be used directly in `WHERE` clauses. This is equivalent to the PostgreSQL `jsonb_path_exists` function.
287+
288+
```typescript
289+
const results = await db
290+
.select()
291+
.from(usersTable)
292+
.where(await protect.jsonbPathExists(usersTable.profile, '$.bio'))
293+
```
294+
295+
### Extract value with `jsonbPathQueryFirst`
296+
297+
Use `jsonbPathQueryFirst` to extract the first value at a given JSONPath in a `SELECT` expression. This is equivalent to the PostgreSQL `jsonb_path_query_first` function.
298+
299+
> [!NOTE]
300+
> `jsonbPathQueryFirst` returns an encrypted value, not a boolean. Use it in `SELECT` expressions, not directly in `WHERE` clauses. Use `jsonbPathExists` to filter rows by path existence.
301+
302+
### Get value with `jsonbGet`
303+
304+
Use `jsonbGet` to get a value using the JSONB `->` operator in a `SELECT` expression.
305+
306+
> [!NOTE]
307+
> `jsonbGet` returns an encrypted value, not a boolean. Use it in `SELECT` expressions, not directly in `WHERE` clauses. Use `jsonbPathExists` to filter rows by path existence.
308+
309+
### Combine JSONB operators with other conditions
310+
311+
JSONB operators can be combined with other Protect operators using `and` and `or`. Use `jsonbPathExists` for boolean conditions in `WHERE` clauses.
312+
313+
```typescript
314+
const results = await db
315+
.select()
316+
.from(usersTable)
317+
.where(
318+
await protect.and(
319+
protect.jsonbPathExists(usersTable.profile, '$.name'),
320+
protect.eq(usersTable.email, 'jane@example.com'),
321+
),
322+
)
323+
```
324+
325+
---
326+
277327
## Aggregation operations
278328

279329
### Count all transactions

docs/reference/searchable-encryption-postgres.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ console.log(term.data) // array of search terms
116116

117117
### JSONB queries with searchableJson (recommended)
118118

119+
> [!TIP]
120+
> **Using Drizzle ORM?** The `@cipherstash/drizzle` package provides higher-level JSONB operators (`jsonbPathQueryFirst`, `jsonbGet`, `jsonbPathExists`) that handle encryption automatically. See the [Drizzle JSONB query examples](./drizzle/drizzle.md#jsonb-queries-with-encrypted-data).
121+
119122
For columns storing JSON data, `.searchableJson()` is the recommended approach. It enables encrypted JSONB queries and automatically infers the correct query operation from the plaintext value type.
120123

121124
Use `encryptQuery` to create encrypted query terms for JSONB columns:

local/create-ci-table.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ CREATE TABLE "protect-ci" (
44
age eql_v2_encrypted,
55
score eql_v2_encrypted,
66
profile eql_v2_encrypted,
7-
created_at TIMESTAMP DEFAULT NOW()
7+
created_at TIMESTAMP DEFAULT NOW(),
8+
test_run_id TEXT
89
);

packages/drizzle/README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ export const usersTable = pgTable('users', {
9494
orderAndRange: true,
9595
}),
9696

97-
// JSON object with typed structure
97+
// JSON object with searchable JSONB queries
9898
profile: encryptedType<{ name: string; bio: string }>('profile', {
9999
dataType: 'json',
100+
searchableJson: true,
100101
}),
101102

102103
createdAt: timestamp('created_at').defaultNow(),
@@ -227,6 +228,31 @@ const decrypted = await protectClient.bulkDecryptModels(results)
227228
> [!IMPORTANT]
228229
> Sorting with ORE on Supabase and other databases that don't support operator families will not work as expected.
229230
231+
### Query encrypted JSONB data
232+
233+
```typescript
234+
// Check if a path exists in encrypted JSONB data
235+
const results = await db
236+
.select()
237+
.from(usersTable)
238+
.where(await protectOps.jsonbPathExists(usersTable.profile, '$.bio'))
239+
240+
// Combine JSONB operators with other conditions
241+
const results = await db
242+
.select()
243+
.from(usersTable)
244+
.where(
245+
await protectOps.and(
246+
protectOps.jsonbPathExists(usersTable.profile, '$.name'),
247+
protectOps.eq(usersTable.email, 'jane@example.com'),
248+
),
249+
)
250+
```
251+
252+
> [!NOTE]
253+
> `jsonbPathExists` returns a boolean and can be used directly in `WHERE` clauses.
254+
> `jsonbPathQueryFirst` and `jsonbGet` return encrypted values, not booleans — use them in `SELECT` expressions, not in `WHERE` clauses.
255+
230256
### Complex queries with mixed operators
231257

232258
```typescript
@@ -275,6 +301,14 @@ All operators automatically handle encryption for encrypted columns.
275301
- `inArray(left, right[])` - In array
276302
- `notInArray(left, right[])` - Not in array
277303

304+
### JSONB Operators (async)
305+
- `jsonbPathQueryFirst(column, selector)` - Extract first value at JSONB path
306+
- `jsonbGet(column, selector)` - Get value using JSONB `->` operator
307+
- `jsonbPathExists(column, selector)` - Check if path exists in JSONB
308+
309+
> [!IMPORTANT]
310+
> JSONB operators require `searchableJson: true` and `dataType: 'json'` in the column's `encryptedType` config.
311+
278312
### Sorting Operators (sync)
279313
- `asc(column)` - Ascending order
280314
- `desc(column)` - Descending order
@@ -304,6 +338,7 @@ Creates an encrypted column type for Drizzle schemas.
304338
- `freeTextSearch?: boolean` - Enable text search (LIKE/ILIKE)
305339
- `equality?: boolean` - Enable equality queries
306340
- `orderAndRange?: boolean` - Enable range queries and sorting
341+
- `searchableJson?: boolean` - Enable JSONB path queries (requires `dataType: 'json'`)
307342

308343
### `extractProtectSchema(table)`
309344

packages/drizzle/__tests__/docs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
createProtectOperators,
1212
encryptedType,
1313
extractProtectSchema,
14-
} from '../src/pg'
14+
} from '@cipherstash/drizzle/pg'
1515
import { docSeedData } from './fixtures/doc-seed-data'
1616
import { type ExecutionContext, executeCodeBlock } from './utils/code-executor'
1717
import { extractExecutableBlocks } from './utils/markdown-parser'

0 commit comments

Comments
 (0)