From f9d045806794d7801236f459f0b9271826497f1f Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Thu, 30 Jul 2026 22:52:04 +1000 Subject: [PATCH] docs(drizzle): add integration API reference --- .github/workflows/docs.yml | 3 + .gitignore | 2 + IA.md | 6 +- .../docs/get-started/choose-your-stack.mdx | 2 +- content/docs/get-started/quickstart.mdx | 2 +- .../drizzle/api-reference/meta.json | 4 ++ .../{drizzle.mdx => drizzle/index.mdx} | 68 +++++++++---------- content/docs/integrations/drizzle/meta.json | 5 ++ content/docs/integrations/meta.json | 2 +- package.json | 3 +- scripts/generate-drizzle-docs.ts | 37 ++++++++++ scripts/validate-content-api.ts | 1 + scripts/validate-links.ts | 1 + src/components/icons/drizzle.tsx | 37 ++++++++++ src/lib/source.ts | 2 + 15 files changed, 134 insertions(+), 41 deletions(-) create mode 100644 content/docs/integrations/drizzle/api-reference/meta.json rename content/docs/integrations/{drizzle.mdx => drizzle/index.mdx} (69%) create mode 100644 content/docs/integrations/drizzle/meta.json create mode 100644 scripts/generate-drizzle-docs.ts create mode 100644 src/components/icons/drizzle.tsx diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 08b68f9..bdf3caf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -52,6 +52,9 @@ jobs: - name: Generate the Supabase reference run: bun run generate-docs:supabase + - name: Generate the Drizzle reference + run: bun run generate-docs:drizzle + - name: Generate the Prisma reference run: bun run generate-docs:prisma diff --git a/.gitignore b/.gitignore index 641a8b8..ac2a70f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,8 @@ content/docs/integrations/prisma/api-reference/* !content/docs/integrations/prisma/api-reference/meta.json content/docs/integrations/supabase/api-reference/* !content/docs/integrations/supabase/api-reference/meta.json +content/docs/integrations/drizzle/api-reference/* +!content/docs/integrations/drizzle/api-reference/meta.json content/docs/reference/stack/api-reference/* !content/docs/reference/stack/api-reference/meta.json .tmp-* diff --git a/IA.md b/IA.md index 38a9f69..c3ef001 100644 --- a/IA.md +++ b/IA.md @@ -159,7 +159,7 @@ Solutions └─ Protecting PII · Healthcare/HIPAA · AI & RAG · Data residency · Provable access Reference ├─ EQL core-concepts · numbers · dates · text · json · … · joins -├─ Stack SDK client · schema · encrypt-decrypt · supabase · drizzle-operators · errors +├─ Stack SDK overview · usage · generated package API ├─ Auth lock-contexts · cts-tokens · oidc · access-keys ├─ CLI · Proxy · Workspace └─ Benchmarks · Agent skills · Glossary @@ -200,7 +200,7 @@ layer, not a layer every Stack page sits on. - Start here: Quickstart · Reference → Stack SDK (client + configuration) - Concepts: application-level encryption · searchable encryption · identity-aware encryption - Guides: schema design · encrypt existing data · testing & CI · serverless & bundling -- Reference: `/reference/stack/*` (schema · encrypt-decrypt · supabase · drizzle-operators · errors) +- Reference: `/reference/stack/*` (overview · usage · generated package API) - Security: `/security/stack-sdk` - Integrations (auto): Supabase · Drizzle · Prisma · DynamoDB · Next.js … @@ -308,7 +308,7 @@ Two notes: - [x] `/integrations/supabase/dashboard-experience` — Table Editor, expose eql schema - [ ] ⛔ `/integrations/supabase/edge-functions` — pending Deno/FFI answer - [ ] ⛔ `/integrations/supabase/realtime` — pending product verification -- [x] `/integrations/drizzle` +- [x] `/integrations/drizzle` — overview and generated `@cipherstash/stack-drizzle` API reference - [x] `/integrations/prisma` — Prisma ORM 8 RC, EQL v3, Prisma Postgres, Prisma Compute, and generated API reference - [ ] `/integrations/aws/rds-aurora` — Proxy path - [ ] `/integrations/aws/dynamodb` diff --git a/content/docs/get-started/choose-your-stack.mdx b/content/docs/get-started/choose-your-stack.mdx index e2c4963..b6cc077 100644 --- a/content/docs/get-started/choose-your-stack.mdx +++ b/content/docs/get-started/choose-your-stack.mdx @@ -44,7 +44,7 @@ Managed platforms usually block custom operator class creation. That's why the d | Query layer | Path | What it looks like | |---|---|---| -| Drizzle | [Drizzle integration](/stack/cipherstash/encryption/drizzle) | An `encryptedType` column and typed query operators. Your Drizzle code keeps its shape. | +| Drizzle | [Drizzle integration](/integrations/drizzle) | Encrypted column types and typed query operators. Your Drizzle code keeps its shape. | | Supabase JS | [Supabase integration](/integrations/supabase) | `encryptedSupabase` wraps the client. `.eq()`, `.gt()`, `.order()` keep working. | | Prisma ORM 8 RC | [Prisma integration](/integrations/prisma) | Encrypted fields and query operators are generated from `schema.prisma`. | | Raw SQL | [Quickstart](/get-started/quickstart) | Encrypt the value, insert it, cast the query operand. | diff --git a/content/docs/get-started/quickstart.mdx b/content/docs/get-started/quickstart.mdx index 086fec5..3368e3d 100644 --- a/content/docs/get-started/quickstart.mdx +++ b/content/docs/get-started/quickstart.mdx @@ -179,7 +179,7 @@ You encrypted a field, stored it, queried it without decrypting, and read it bac ## Next steps -- **Using an ORM or Supabase?** The [Drizzle](/stack/cipherstash/encryption/drizzle) and [Supabase](/integrations/supabase) integrations wrap your existing client so queries look normal. +- **Using an ORM or Supabase?** The [Drizzle](/integrations/drizzle) and [Supabase](/integrations/supabase) integrations wrap your existing client so queries look normal. - **No app changes possible?** [CipherStash Proxy](/reference/proxy) sits in front of Postgres and does this transparently. - **Bind decryption to a user.** [Provable access control](/solutions/provable-access) ties a value to the identity that encrypted it. - **Ready to deploy?** [Deployment](/guides/deployment) covers credential choices, environment promotion, and production rollout gates. diff --git a/content/docs/integrations/drizzle/api-reference/meta.json b/content/docs/integrations/drizzle/api-reference/meta.json new file mode 100644 index 0000000..dd99951 --- /dev/null +++ b/content/docs/integrations/drizzle/api-reference/meta.json @@ -0,0 +1,4 @@ +{ + "title": "API reference", + "pages": ["index"] +} diff --git a/content/docs/integrations/drizzle.mdx b/content/docs/integrations/drizzle/index.mdx similarity index 69% rename from content/docs/integrations/drizzle.mdx rename to content/docs/integrations/drizzle/index.mdx index e099aca..7900097 100644 --- a/content/docs/integrations/drizzle.mdx +++ b/content/docs/integrations/drizzle/index.mdx @@ -1,5 +1,5 @@ --- -title: Drizzle +title: Overview description: "Encrypted columns with Drizzle ORM: pick a concrete EQL column type, and capability-checked operators encrypt your query operands for you." type: tutorial components: [encryption, eql] @@ -9,8 +9,8 @@ integration: setup: code-only pairsWith: [supabase, prisma] verifiedAgainst: - stack: "1.0.0-rc.0" - eql: "3.0.0" + stack: "1.0.0-rc.4" + eql: "3.0.4" --- Drizzle keeps its shape. You declare each encrypted column with a concrete EQL type, and a set of query operators encrypt their operands before Drizzle builds the SQL. Your `select`, `where`, and `orderBy` read as they always did, and the database only ever compares ciphertext. @@ -39,19 +39,23 @@ The native Stack client used below discovers the four `CS_*` variables first, th ## Declare the table -The column type *is* the capability. There are no flags to configure. `TextEq` answers equality and nothing else, `IntegerOrd` answers ranges and ordering, `TextMatch` answers free-text containment, and a bare `Text` or `Bigint` is storage-only. +The column type *is* the capability. There are no flags to configure. `TextEq` answers equality and nothing else, `IntegerOrd` answers ranges and ordering, `TextMatch` answers free-text matching, and a bare `Text` or `Bigint` is storage-only. ```typescript filename="src/schema.ts" import { pgTable, integer } from "drizzle-orm/pg-core" -import { types } from "@cipherstash/stack-drizzle/v3" - -export const users = pgTable("users", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - email: types.TextEq("email"), // equality - age: types.IntegerOrd("age"), // ranges, ordering, and equality - bio: types.TextMatch("bio"), // free-text containment - balance: types.Bigint("balance"), // storage only -}) +import { encryptedIndexes, types } from "@cipherstash/stack-drizzle" + +export const users = pgTable( + "users", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + email: types.TextEq("email"), // equality + age: types.IntegerOrd("age"), // ranges, ordering, and equality + bio: types.TextMatch("bio"), // free-text token matching + balance: types.Bigint("balance"), // storage only + }, + (table) => [...encryptedIndexes(table)], +) ``` Each factory maps to the matching EQL domain, so `types.TextEq("email")` types the column as `public.eql_v3_text_eq`. The variant model behind those names is in [core concepts](/reference/eql/core-concepts). @@ -61,7 +65,7 @@ Each factory maps to the matching EQL domain, so `types.TextEq("email")` types t | `types.Text(name)` | Store and decrypt only | `public.eql_v3_text` | | `types.TextEq(name)` | `eq`, `ne`, `inArray`, `notInArray` | `public.eql_v3_text_eq` | | `types.TextOrd(name)` | Ranges, ordering, plus equality | `public.eql_v3_text_ord` | -| `types.TextMatch(name)` | `contains` | `public.eql_v3_text_match` | +| `types.TextMatch(name)` | `matches` | `public.eql_v3_text_match` | | `types.TextSearch(name)` | All of the above, on text | `public.eql_v3_text_search` | | `types.IntegerOrd(name)` | Ranges, ordering, plus equality | `public.eql_v3_integer_ord` | @@ -76,21 +80,21 @@ Derive the encryption schema from the Drizzle table, build a typed client, and c ```typescript filename="src/db.ts" import { drizzle } from "drizzle-orm/postgres-js" import { - createEncryptionOperatorsV3, - extractEncryptionSchemaV3, -} from "@cipherstash/stack-drizzle/v3" -import { EncryptionV3 } from "@cipherstash/stack/v3" + createEncryptionOperators, + extractEncryptionSchema, +} from "@cipherstash/stack-drizzle" +import { Encryption } from "@cipherstash/stack" import { users } from "./schema" -export const usersSchema = extractEncryptionSchemaV3(users) +export const usersSchema = extractEncryptionSchema(users) -export const client = await EncryptionV3({ schemas: [usersSchema] }) -export const ops = createEncryptionOperatorsV3(client) +export const client = await Encryption({ schemas: [usersSchema] }) +export const ops = createEncryptionOperators(client) export const db = drizzle({ client: sqlClient }) ``` -`extractEncryptionSchemaV3` reads the encryption config straight off the column types, so there is no second schema to keep in sync with the first. +`extractEncryptionSchema` reads the encryption config straight off the column types, so there is no second schema to keep in sync with the first. ## Query @@ -118,15 +122,15 @@ const listed = await db.select().from(users) // Free-text token containment — bio is TextMatch const coffee = await db.select().from(users) - .where(await ops.contains(users.bio, "coffee")) + .where(await ops.matches(users.bio, "coffee")) ``` -The full operator set is `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `notBetween`, `inArray`, `notInArray`, `contains`, `exists`, `notExists`, `and`, `or`, `not`, `isNull`, `isNotNull`, `asc`, and `desc`. Null checks need no encryption, because a SQL `NULL` is never encrypted. +The full operator set is `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `between`, `notBetween`, `matches`, `contains`, `selector`, `inArray`, `notInArray`, `exists`, `notExists`, `and`, `or`, `not`, `isNull`, `isNotNull`, `asc`, and `desc`. Null checks need no encryption, because a SQL `NULL` is never encrypted. See the [API reference](/integrations/drizzle/api-reference#createencryptionoperators) for signatures and per-operator capability requirements. Applying an operator the column's type cannot answer throws `EncryptionOperatorError` rather than silently returning wrong rows. Ordering a `TextEq` column, for instance, has no ordering term to compare. -There is no `like` or `ilike`. SQL pattern matching is meaningless on ciphertext, so free-text search is `ops.contains`, which compares encrypted token sets. It matches whole indexed tokens rather than arbitrary substrings. +There is no `like` or `ilike`. SQL pattern matching is meaningless on ciphertext, so free-text search is `ops.matches`, which compares encrypted token sets. It matches whole indexed tokens rather than arbitrary substrings. `ops.contains` is reserved for encrypted JSON containment. ## Insert and read @@ -156,20 +160,16 @@ await db.insert(users).values(rows.data) ## Indexes -At real row counts, add a functional index for each capability you query. The encrypted operators inline into these extractor functions, so an ordinary `CREATE INDEX` is all it takes: - -```sql filename="indexes.sql" -CREATE INDEX users_email_eq ON users USING hash (eql_v3.eq_term(email)); -CREATE INDEX users_age_ord ON users USING btree (eql_v3.ord_term(age)); -CREATE INDEX users_bio_match ON users USING gin (eql_v3.match_term(bio)); -ANALYZE users; -``` +`encryptedIndexes(table)` derives the recommended functional indexes from each encrypted column's capabilities. In the schema above it produces equality, ordering, and free-text indexes while ignoring the storage-only `balance` column. Generate and apply the result through your normal `drizzle-kit` migration workflow, then run `ANALYZE users`. -Index selection, `EXPLAIN` verification, and large-table build guidance are in [EQL indexes](/reference/eql/indexes). +See [`encryptedIndexes`](/integrations/drizzle/api-reference#encryptedindexes) for the emitted index forms. Index selection, `EXPLAIN` verification, and large-table build guidance are in [EQL indexes](/reference/eql/indexes). ## Where to next + + Exported column types, schema helpers, codecs, indexes, and encrypted query operators. + Drizzle over a Supabase Postgres connection. diff --git a/content/docs/integrations/drizzle/meta.json b/content/docs/integrations/drizzle/meta.json new file mode 100644 index 0000000..d185819 --- /dev/null +++ b/content/docs/integrations/drizzle/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Drizzle", + "icon": "Drizzle", + "pages": ["index", "api-reference"] +} diff --git a/content/docs/integrations/meta.json b/content/docs/integrations/meta.json index 15136e6..78d30fa 100644 --- a/content/docs/integrations/meta.json +++ b/content/docs/integrations/meta.json @@ -1,5 +1,5 @@ { "title": "Integrations", "icon": "Blocks", - "pages": ["index", "supabase", "prisma", "..."] + "pages": ["index", "supabase", "prisma", "drizzle", "..."] } diff --git a/package.json b/package.json index 2b9a412..f851fd1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "prebuild": "bun run generate-docs && bun run generate-docs:supabase && bun run generate-docs:prisma && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects", + "prebuild": "bun run generate-docs && bun run generate-docs:supabase && bun run generate-docs:drizzle && bun run generate-docs:prisma && bun run generate-docs:eql && bun run generate-docs:eql-api && bun run generate-docs:cli && bun run validate-content && bun run validate-mermaid && bun run validate-links && bun run validate-redirects", "build": "next build", "dev": "next dev -p 3001", "start": "next start", @@ -13,6 +13,7 @@ "format": "biome format --write", "generate-docs": "tsx scripts/generate-docs.ts", "generate-docs:supabase": "tsx scripts/generate-supabase-docs.ts", + "generate-docs:drizzle": "tsx scripts/generate-drizzle-docs.ts", "generate-docs:prisma": "tsx scripts/generate-prisma-docs.ts", "generate-docs:eql": "tsx scripts/generate-eql-docs.ts", "generate-docs:eql-api": "tsx scripts/generate-eql-api-docs.ts", diff --git a/scripts/generate-drizzle-docs.ts b/scripts/generate-drizzle-docs.ts new file mode 100644 index 0000000..a66af22 --- /dev/null +++ b/scripts/generate-drizzle-docs.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env tsx +/** Generate the Drizzle ORM integration's API reference from Stack main. */ +import path from "node:path"; +import { type DocsConfig, generateDocs } from "./lib/docs-generator.js"; + +const drizzleConfig: DocsConfig = { + packageName: "@cipherstash/stack-drizzle", + projectName: "@cipherstash/stack-drizzle", + repoUrl: "https://github.com/cipherstash/stack.git", + sourceRef: "main", + tempDirName: ".tmp-stack-drizzle", + baseOutputDir: path.join( + process.cwd(), + "content/docs/integrations/drizzle/api-reference", + ), + publicPath: "/integrations/drizzle/api-reference", + metaTitle: "API reference", + versionedOutput: false, + entryPointBasePath: "packages/stack-drizzle/src", + entryModule: "index", + router: "module", + flattenOutputFiles: true, + entryPoints: ["./packages/stack-drizzle/src/index.ts"], + tsconfigInclude: ["packages/stack-drizzle/src/**/*"], + tagFilter: () => false, + referencePathSegment: "drizzle", + frontmatterGlobals: { + type: "reference", + components: ["encryption", "eql"], + audience: ["developer"], + }, +}; + +generateDocs(drizzleConfig).catch((error) => { + console.error("Failed to generate the Drizzle API reference:", error); + process.exit(1); +}); diff --git a/scripts/validate-content-api.ts b/scripts/validate-content-api.ts index 56fff7c..4490f1b 100644 --- a/scripts/validate-content-api.ts +++ b/scripts/validate-content-api.ts @@ -37,6 +37,7 @@ const SKIP_PATHS = [ "content/stack/reference/stack/", // TypeDoc output "content/docs/reference/stack/api-reference/", // TypeDoc output "content/docs/integrations/supabase/api-reference/", // TypeDoc output + "content/docs/integrations/drizzle/api-reference/", // TypeDoc output "content/docs/integrations/prisma/api-reference/", // TypeDoc output "content/stack/reference/eql/index.mdx", // generated EQL API reference "content/docs/reference/eql/functions.mdx", // generated diff --git a/scripts/validate-links.ts b/scripts/validate-links.ts index 1ba7014..b4c34e9 100644 --- a/scripts/validate-links.ts +++ b/scripts/validate-links.ts @@ -361,6 +361,7 @@ allBroken.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line); const GENERATED = [ "content/docs/reference/stack/api-reference/index.mdx", "content/docs/integrations/supabase/api-reference/index.mdx", + "content/docs/integrations/drizzle/api-reference/index.mdx", "content/docs/integrations/prisma/api-reference/index.mdx", ]; const missing = GENERATED.filter((d) => !fs.existsSync(path.join(ROOT, d))); diff --git a/src/components/icons/drizzle.tsx b/src/components/icons/drizzle.tsx new file mode 100644 index 0000000..9c4ee5e --- /dev/null +++ b/src/components/icons/drizzle.tsx @@ -0,0 +1,37 @@ +export function DrizzleIcon(props: React.SVGProps) { + return ( + + + + + + + ); +} diff --git a/src/lib/source.ts b/src/lib/source.ts index 6c2080d..7c990b3 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -3,10 +3,12 @@ import type * as PageTree from "fumadocs-core/page-tree"; import { type InferPageType, loader } from "fumadocs-core/source"; import { icons } from "lucide-react"; import { createElement } from "react"; +import { DrizzleIcon } from "@/components/icons/drizzle"; import { PrismaIcon } from "@/components/icons/prisma"; import { SupabaseIcon } from "@/components/icons/supabase"; const customIcons: Record React.ReactElement> = { + Drizzle: () => createElement(DrizzleIcon, { width: 16, height: 16 }), Prisma: () => createElement(PrismaIcon, { width: 16, height: 16 }), Supabase: () => createElement(SupabaseIcon, { width: 16, height: 16 }), };