Skip to content

Commit 7feaa52

Browse files
calvinbrewerclaude
authored andcommitted
feat(prisma-example): adopt the eql* operator surface, regenerate the contract
Rename every v3 operator call to the EQL-derived vocabulary (eqlEq, eqlMatch, eqlAsc/eqlDesc, …), fully qualify the README's domain table (public.eql_v3_*), describe free-text search as fuzzy bloom token matching under its real needle semantics, drop the removed cipherstashNotIlike coverage, add a direct dotenv/config import to the str-range suite, and remove the type-erasing probe assertions in the bool suite. pnpm emit regenerates contract.{json,d.ts} — the extensionPacks.cipherstash block now carries the 40 v3 codec instances and storage rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cf13a34 commit 7feaa52

14 files changed

Lines changed: 2692 additions & 178 deletions

.changeset/prisma-example-eql-v3.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@cipherstash/prisma-next-example': minor
33
---
44

5-
Convert the example app to EQL v3. Every column is now a concrete `public.eql_v3_*` domain authored with the per-domain constructors (`EncryptedTextSearch`, `EncryptedDoubleOrd`, `EncryptedBigIntOrd`, `EncryptedDateOrd`, `EncryptedBoolean`, `EncryptedJson`), wired through `cipherstashFromStackV3({ contractJson })`. The e2e harness runs the full v3 surface against live Postgres + ZeroKMS with no skips: equality/range/free-text-token operators, `cipherstashV3Asc`/`Desc` order-term sorting, encrypted JSON containment (`cipherstashJsonContains` — the v2 `cipherstashJsonb*` helpers do not exist in v3), lossless `bigint` beyond `Number.MAX_SAFE_INTEGER`, and the storage-only `eql_v3_boolean` refusal (`EncryptionOperatorError`) pinned as a feature. Migrations regenerate from the v3 contract: the initial app migration creates the `users` table against the v3 domains with zero `add_search_config` ops, and the cipherstash space carries both bundle baselines (v2 + v3).
5+
Convert the example app to EQL v3. Every column is now a concrete `public.eql_v3_*` domain authored with the per-domain constructors (`EncryptedTextSearch`, `EncryptedDoubleOrd`, `EncryptedBigIntOrd`, `EncryptedDateOrd`, `EncryptedBoolean`, `EncryptedJson`), wired through `cipherstashFromStackV3({ contractJson })`. The e2e harness runs the full v3 surface against live Postgres + ZeroKMS with no skips: the `eql*` operator vocabulary (equality/range plus `eqlMatch` free-text token search), `eqlAsc`/`eqlDesc` order-term sorting, encrypted JSON containment (`eqlJsonContains` — the v2 `cipherstashJsonb*` helpers do not exist in v3), lossless `bigint` beyond `Number.MAX_SAFE_INTEGER`, and the storage-only `eql_v3_boolean` refusal (`EncryptionOperatorError`) pinned as a feature. Migrations regenerate from the v3 contract: the initial app migration creates the `users` table against the v3 domains with zero `add_search_config` ops, and the cipherstash space carries both bundle baselines (v2 + v3).

examples/prisma/README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ End-to-end demo of [`@cipherstash/prisma-next`](../../packages/prisma-next/READM
44

55
A single `User` model with one column per plaintext family, exercised end-to-end: insert, equality, free-text token search, range, between, in-array, encrypted-order-term sort, JSON containment, and `decryptAll`-amortised read.
66

7-
| Column | Constructor | Domain | Query surface |
8-
| --------------- | --------------------------------- | --------------------- | -------------------------------------- |
9-
| `email` | `cipherstash.EncryptedTextSearch()` | `eql_v3_text_search` | equality + order/range + free-text |
10-
| `salary` | `cipherstash.EncryptedDoubleOrd()` | `eql_v3_double_ord` | equality + order/range |
11-
| `accountId` | `cipherstash.EncryptedBigIntOrd()` | `eql_v3_bigint_ord` | equality + order/range (true `bigint`) |
12-
| `birthday` | `cipherstash.EncryptedDateOrd()` | `eql_v3_date_ord` | equality + order/range |
13-
| `emailVerified` | `cipherstash.EncryptedBoolean()` | `eql_v3_boolean` | storage-only (no operators) |
14-
| `preferences` | `cipherstash.EncryptedJson()` | `eql_v3_json` | `cipherstashJsonContains` (`@>`) |
7+
| Column | Constructor | Domain | Query surface |
8+
| --------------- | --------------------------------- | --------------------------- | -------------------------------------- |
9+
| `email` | `cipherstash.EncryptedTextSearch()` | `public.eql_v3_text_search` | equality + order/range + free-text (`eqlMatch`) |
10+
| `salary` | `cipherstash.EncryptedDoubleOrd()` | `public.eql_v3_double_ord` | equality + order/range |
11+
| `accountId` | `cipherstash.EncryptedBigIntOrd()` | `public.eql_v3_bigint_ord` | equality + order/range (true `bigint`) |
12+
| `birthday` | `cipherstash.EncryptedDateOrd()` | `public.eql_v3_date_ord` | equality + order/range |
13+
| `emailVerified` | `cipherstash.EncryptedBoolean()` | `public.eql_v3_boolean` | storage-only (no operators) |
14+
| `preferences` | `cipherstash.EncryptedJson()` | `public.eql_v3_json` | `eqlJsonContains` (`@>`) |
1515

1616
📖 See the [Prisma Next encryption docs](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next) for the full operator reference, security model, and known limitations.
1717

@@ -64,36 +64,36 @@ pnpm install && pnpm emit && pnpm typecheck
6464
--- Insert (mixed-domain round-trip) ---
6565
Inserted 4 rows across six cipherstash v3 domains.
6666
67-
--- cipherstashEq (text_search equality) ---
67+
--- eqlEq (text_search equality) ---
6868
Found 1 row(s) for alice@example.com.
6969
user-0: alice@example.com
7070
71-
--- cipherstashIlike (text_search free-text tokens) ---
71+
--- eqlMatch (text_search free-text tokens) ---
7272
Found 3 row(s) whose email contains example.com.
7373
user-0: alice@example.com
7474
user-1: bob@example.com
7575
user-2: carol@example.com
7676
77-
--- cipherstashGt (double_ord order-and-range) ---
77+
--- eqlGt (double_ord order-and-range) ---
7878
Found 2 user(s) with salary > 100,000.
7979
user-1: salary=110000
8080
user-3: salary=145000
8181
82-
--- cipherstashBetween (date_ord order-and-range) ---
82+
--- eqlBetween (date_ord order-and-range) ---
8383
Found 3 user(s) born between 1985 and 1995.
8484
85-
--- cipherstashInArray (bigint_ord equality) ---
85+
--- eqlIn (bigint_ord equality) ---
8686
Found 2 user(s) whose accountId is in the supplied array.
8787
8888
--- eql_v3_boolean (storage-only round-trip) ---
8989
user-2: emailVerified=false
9090
91-
--- cipherstashJsonContains (encrypted jsonb @>) ---
91+
--- eqlJsonContains (encrypted jsonb @>) ---
9292
Found 2 user(s) with a dark-theme preference.
9393
user-0: {"theme":"dark","notifications":true}
9494
user-2: {"theme":"dark","notifications":true}
9595
96-
--- cipherstashV3Asc (encrypted order-term ORDER BY) ---
96+
--- eqlAsc (encrypted order-term ORDER BY) ---
9797
user-0: email=alice@example.com
9898
user-1: email=bob@example.com
9999
user-2: email=carol@example.com
@@ -102,7 +102,7 @@ Found 2 user(s) with a dark-theme preference.
102102

103103
Two v3 behaviours worth noticing in that output:
104104

105-
- **`cipherstashIlike` is bloom-filter token containment** (`eql_v3.contains`), not SQL `ILIKE` — the needle's tokens must appear in the ciphertext's index. Plain substrings like `example.com` are the idiomatic needle; `%` wildcards are not part of the model.
105+
- **`eqlMatch` is fuzzy bloom-filter token matching** (`eql_v3.contains`), not SQL `ILIKE` — the needle's tokens must appear in the ciphertext's index. Plain literal terms like `example.com` are the idiomatic needle: leading/trailing `%` are stripped for compatibility, while an interior `%` or any `_` throws (the tokenizer would treat them as ordinary characters), as do needles shorter than the match tokenizer length.
106106
- **`eql_v3_boolean` is storage-only.** It round-trips `true`/`false` losslessly but surfaces no search operators — calling one throws `EncryptionOperatorError`. Filter on a searchable column and decrypt the boolean from the result set.
107107

108108
## References

examples/prisma/prisma/schema.prisma

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
//
1111
// - email (EncryptedTextSearch → eql_v3_text_search)
1212
// equality + order/range + free-text search:
13-
// cipherstashEq / Ne / InArray / NotInArray /
13+
// eqlEq / Neq / In / NotIn /
1414
// Gt / Gte / Lt / Lte / Between / NotBetween /
15-
// Ilike / NotIlike, cipherstashV3Asc / Desc.
15+
// Match, eqlAsc / Desc.
1616
// - salary (EncryptedDoubleOrd → eql_v3_double_ord)
1717
// equality + order/range.
1818
// - accountId (EncryptedBigIntOrd → eql_v3_bigint_ord)
@@ -21,10 +21,10 @@
2121
// equality + order/range.
2222
// - emailVerified (EncryptedBoolean → eql_v3_boolean)
2323
// STORAGE-ONLY: no search operators at all — every
24-
// cipherstash* predicate throws
24+
// eql* predicate throws
2525
// EncryptionOperatorError on this column.
2626
// - preferences (EncryptedJson → eql_v3_json)
27-
// searchable JSON: cipherstashJsonContains
27+
// searchable JSON: eqlJsonContains
2828
// (encrypted jsonb `@>` containment).
2929
//
3030
// v3 emits NO search configs: the domain carries its own index

examples/prisma/src/index.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* - accountId eql_v3_bigint_ord — eq + range + ORDER BY
2121
* - birthday eql_v3_date_ord — eq + range + ORDER BY
2222
* - emailVerified eql_v3_boolean — STORAGE-ONLY (no operators)
23-
* - preferences eql_v3_json — cipherstashJsonContains (@>)
23+
* - preferences eql_v3_json — eqlJsonContains (@>)
2424
*
2525
* Prerequisites:
2626
*
@@ -39,14 +39,14 @@
3939
import 'dotenv/config'
4040

4141
import {
42-
cipherstashV3Asc,
4342
decryptAll,
4443
EncryptedBigInt,
4544
EncryptedBoolean,
4645
EncryptedDate,
4746
EncryptedJson,
4847
EncryptedNumber,
4948
EncryptedString,
49+
eqlAsc,
5050
} from '@cipherstash/prisma-next/runtime'
5151

5252
import { db } from './db'
@@ -117,7 +117,7 @@ async function main() {
117117
await clearUsers()
118118
await insertUsers()
119119
await searchByEq()
120-
await searchByIlikeAndDecrypt()
120+
await searchByMatchAndDecrypt()
121121
await rangeQueryOnSalary()
122122
await betweenQueryOnBirthday()
123123
await inArrayQueryOnAccountId()
@@ -159,9 +159,9 @@ async function insertUsers(): Promise<void> {
159159
}
160160

161161
async function searchByEq(): Promise<void> {
162-
console.log('\n--- cipherstashEq (text_search equality) ---')
162+
console.log('\n--- eqlEq (text_search equality) ---')
163163
const rows = await db.orm.public.User.where((u) =>
164-
u.email.cipherstashEq('alice@example.com'),
164+
u.email.eqlEq('alice@example.com'),
165165
).all()
166166
console.log(`Found ${rows.length} row(s) for alice@example.com.`)
167167
await decryptAll(rows)
@@ -170,13 +170,13 @@ async function searchByEq(): Promise<void> {
170170
}
171171
}
172172

173-
async function searchByIlikeAndDecrypt(): Promise<void> {
174-
console.log('\n--- cipherstashIlike (text_search free-text tokens) ---')
173+
async function searchByMatchAndDecrypt(): Promise<void> {
174+
console.log('\n--- eqlMatch (text_search free-text tokens) ---')
175175
// v3 free-text search is bloom-filter TOKEN matching (eql_v3.contains),
176176
// not SQL ILIKE: the needle's tokens must appear in the ciphertext's
177177
// index. 'example.com' matches the three @example.com addresses.
178178
const rows = await db.orm.public.User.where((u) =>
179-
u.email.cipherstashIlike('example.com'),
179+
u.email.eqlMatch('example.com'),
180180
).all()
181181
console.log(`Found ${rows.length} row(s) whose email contains example.com.`)
182182
await decryptAll(rows)
@@ -186,9 +186,9 @@ async function searchByIlikeAndDecrypt(): Promise<void> {
186186
}
187187

188188
async function rangeQueryOnSalary(): Promise<void> {
189-
console.log('\n--- cipherstashGt (double_ord order-and-range) ---')
189+
console.log('\n--- eqlGt (double_ord order-and-range) ---')
190190
const rows = await db.orm.public.User.where((u) =>
191-
u.salary.cipherstashGt(100_000),
191+
u.salary.eqlGt(100_000),
192192
).all()
193193
console.log(`Found ${rows.length} user(s) with salary > 100,000.`)
194194
await decryptAll(rows)
@@ -198,19 +198,19 @@ async function rangeQueryOnSalary(): Promise<void> {
198198
}
199199

200200
async function betweenQueryOnBirthday(): Promise<void> {
201-
console.log('\n--- cipherstashBetween (date_ord order-and-range) ---')
201+
console.log('\n--- eqlBetween (date_ord order-and-range) ---')
202202
const lower = new Date('1985-01-01')
203203
const upper = new Date('1995-12-31')
204204
const rows = await db.orm.public.User.where((u) =>
205-
u.birthday.cipherstashBetween(lower, upper),
205+
u.birthday.eqlBetween(lower, upper),
206206
).all()
207207
console.log(`Found ${rows.length} user(s) born between 1985 and 1995.`)
208208
}
209209

210210
async function inArrayQueryOnAccountId(): Promise<void> {
211-
console.log('\n--- cipherstashInArray (bigint_ord equality) ---')
211+
console.log('\n--- eqlIn (bigint_ord equality) ---')
212212
const rows = await db.orm.public.User.where((u) =>
213-
u.accountId.cipherstashInArray([100_000_000_001n, 100_000_000_004n]),
213+
u.accountId.eqlIn([100_000_000_001n, 100_000_000_004n]),
214214
).all()
215215
console.log(
216216
`Found ${rows.length} user(s) whose accountId is in the supplied array.`,
@@ -224,7 +224,7 @@ async function decryptStorageOnlyBoolean(): Promise<void> {
224224
// calling one would throw EncryptionOperatorError. Filter on a
225225
// searchable column and decrypt the boolean from the result set.
226226
const rows = await db.orm.public.User.where((u) =>
227-
u.email.cipherstashEq('carol@example.com'),
227+
u.email.eqlEq('carol@example.com'),
228228
).all()
229229
await decryptAll(rows)
230230
for (const row of rows) {
@@ -235,9 +235,9 @@ async function decryptStorageOnlyBoolean(): Promise<void> {
235235
}
236236

237237
async function jsonContainmentOnPreferences(): Promise<void> {
238-
console.log('\n--- cipherstashJsonContains (encrypted jsonb @>) ---')
238+
console.log('\n--- eqlJsonContains (encrypted jsonb @>) ---')
239239
const rows = await db.orm.public.User.where((u) =>
240-
u.preferences.cipherstashJsonContains({ theme: 'dark' }),
240+
u.preferences.eqlJsonContains({ theme: 'dark' }),
241241
).all()
242242
console.log(`Found ${rows.length} user(s) with a dark-theme preference.`)
243243
await decryptAll(rows)
@@ -249,10 +249,8 @@ async function jsonContainmentOnPreferences(): Promise<void> {
249249
}
250250

251251
async function sortByEmailAsc(): Promise<void> {
252-
console.log('\n--- cipherstashV3Asc (encrypted order-term ORDER BY) ---')
253-
const rows = await db.orm.public.User.orderBy((u) =>
254-
cipherstashV3Asc(u.email),
255-
).all()
252+
console.log('\n--- eqlAsc (encrypted order-term ORDER BY) ---')
253+
const rows = await db.orm.public.User.orderBy((u) => eqlAsc(u.email)).all()
256254
await decryptAll(rows)
257255
for (const row of rows) {
258256
console.log(` ${row.id}: email=${await row.email.decrypt()}`)

0 commit comments

Comments
 (0)