feat(stack): EQL v3 Prisma ORM integration — CIP-3302#566
Conversation
Spike-validated design for vanilla Prisma (@prisma/client) support at the same layer as the Supabase and Drizzle v3 integrations. Key findings: Json fields over domain-typed columns round-trip CRUD; Prisma's typed where casts the column side to jsonb and bypasses the domain operators, so all encrypted filtering lowers to raw SQL; plain null on Json fields writes JSON null and fails the domain CHECK (must normalize to DbNull).
🦋 Changeset detectedLatest commit: e1833a4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
#565 ships the sibling integration but on the protect-ffi 0.26 / pre-rename-domain bundle line, while main (this module's base) is 0.27 + SQL-standard names with term constructors in eql_v3_internal and scalar encryptQuery unsupported — so its exact SQL and term-operand path do not transfer. Adopt its dialect shape instead: gating on build().indexes, equality-via-ORE fallback, encrypted ORDER BY (now in scope as a fragment builder), whereIn with bounded operand-encryption concurrency, and the no-silent-fallback error convention. Free-text match pinned to the two-arg eql_v3.contains form; sequencing no longer blocks on #565.
|
Design doc updated with insights from the Drizzle v3 implementation (#565) — new §2b. Headline: #565 does not transfer wholesale. It targets the protect-ffi 0.26 / pre-rename-domain bundle line ( What was adopted from #565: capability gating on One deliberate divergence: #565's codec maps |
Implements the design doc: @cipherstash/stack/eql/v3/prisma exporting
encryptedPrisma({ encryptionClient, prismaClient, prisma, tables }) →
{ client, where, $queryRawEncrypted }.
- $extends query hook: encrypt-on-write (create/update/upsert/createMany/
updateMany + AndReturn variants) via encryptModel/bulkEncryptModels,
null → Prisma.DbNull (domain CHECK rejects JSON null), undefined
preserved as "not provided"; decrypt-on-read with Date reconstruction.
- Typed-query guard: encrypted columns in where/orderBy/distinct/cursor/
having throw PrismaEncryptedColumnError (Prisma's Json lowering casts
the column side to jsonb, bypassing the eql_v3 operators — typed
filters would silently match nothing).
- Capability-checked Prisma.sql fragment builders gated on
build().indexes: eq/ne/gt/gte/lt/lte/between/notBetween/contains/in/
notIn/orderBy/isNull/isNotNull, lowering to the two-arg eql_v3.*
function forms with full-envelope operands (interim — single swap
point for CIP-3402/CIP-3423), equality-via-ORE on ord-only columns
handled by the bundle's per-domain functions.
- $queryRawEncrypted decrypts raw rows (db-name keyed) against any v3
table; lock context + audit passthrough at config and call level.
- Prisma namespace (sql, DbNull) is caller-injected: Prisma 7 generates
the client to a user-chosen path, so the library cannot resolve it.
72 unit tests + type tests + CJS bundle coverage; package export,
tsup entry, changeset (minor).
Summary
Adds EQL v3 support for vanilla Prisma ORM (
@prisma/client) to@cipherstash/stack, at the same layer as the Supabase integration (src/supabase) and the Drizzle v3 integration (#565). Not to be confused with@cipherstash/prisma-next(#515) — that's the in-house ORM, a separate track.Current state: design doc only (
docs/superpowers/specs/2026-07-06-eql-v3-prisma-design.md), validated by a live spike. Implementation lands in follow-up commits on this branch.Spike findings (Prisma 7.8.0 +
@prisma/adapter-pg, PG 17 + eql_v3 bundle)Jsonfields over domain-typed DB columns work for the full CRUD surface —create/createMany/update/updateMany/findManyround-trip envelopes; PG assignment-casts jsonb→domain and the CHECK validates on write.Unsupported("eql_v3.…")rejected (removes the field from the generated client).whereis a dead end: Prisma lowers Jsonequalsto"col"::jsonb = $1— the column-side cast bypasses the domain's custom=, so comparison is structural (silent zero rows with real ciphertexts). Range ops on Json don't exist. The extension must throw on encrypted columns in typedwhere.$queryRaw: native domain operator (email = $1::jsonb) and extracted terms (eql_v3.eq_term(email) = eql_v3.eq_term($1::jsonb::eql_v3.text_eq)). Design picks the extracted-term dialect shared with Add EQL v3 Drizzle support #565.nullon a Json field writes JSONnull→ fails the domain CHECK; must normalize toPrisma.DbNull.Planned architecture (per design doc)
packages/stack/src/eql/v3/prisma/exported at@cipherstash/stack/eql/v3/prisma:$extendsquery component: transparent encrypt-on-write / decrypt-on-read (encryptModel/bulkDecryptModels, Date/bigint reconstruction, null→DbNull, lock context + audit passthrough)Prisma.sqlwhere-fragment builders +$queryRawEncrypted, interim full-envelope operands with the CIP-3402 single-swap-point disciplineSequencing
Test plan
.test-d.tstype tests, capability-mismatch matrixDATABASE_URL+CS_*)examples/(Prisma 7, driver-adapter shaped)Refs CIP-3302