Skip to content

fix(cli): scaffold EQL v3 encryption client from stash init#734

Merged
tobyhede merged 1 commit into
mainfrom
fix/init-placeholder-eql-v3
Jul 21, 2026
Merged

fix(cli): scaffold EQL v3 encryption client from stash init#734
tobyhede merged 1 commit into
mainfrom
fix/init-placeholder-eql-v3

Conversation

@tobyhede

@tobyhede tobyhede commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes #733

stash init installs an EQL v3 database (since #705) but the client scaffold it writes into the customer's repo still taught v2Encryption, encryptedColumn('x').equality().freeTextSearch(), encryptedType<T>('x', { equality: true }). The scaffold and the database disagreed, and the scaffold is what the customer's coding agent reads to do the real schema work.

What changed

Two live surfaces in init/utils.ts ported to v3:

  • Placeholders (DRIZZLE_PLACEHOLDER / GENERIC_PLACEHOLDER) — what stash init and the eql install safety net write. The inline pattern-reference comments now show EncryptionV3, types.*, and the @cipherstash/stack-drizzle/v3 entry.
  • generateClientFromSchemas (Drizzle + generic) — reached via stash schema build.

A shared helper maps a SearchOp set to the fixed-capability v3 domain, verified against packages/stack/src/eql/v3/columns.ts:

dataType searchOps v3 factory
string none / eq / ord / free-text / eq+ord+text Text / TextEq / TextOrd / TextMatch / TextSearch
number none / eq / ord Integer / IntegerEq / IntegerOrd
date none / eq / ord Date / DateEq / DateOrd
boolean any Boolean (storage-only)
json any Json

Deleted, not ported

generateClientFromSchema (singular) + generateDrizzleFromSchema + generateSchemaFromDef + drizzleTsTypezero callers anywhere in packages/. (stash schema build imports the plural.) Porting unreachable v2 code to teach v3 would just be dead code in a new dialect.

Notes

  • v3 capability difference surfaced: v2 allowed equality on a boolean; v3 has no boolean eq/ord domain, so booleans map to storage-only.
  • Numeric collapses to Integer* — there's no width/float signal in the introspected SearchOp model (v2 collapsed everything to dataType: 'number' too). Distinct float/decimal scaffolding needs a richer introspection signal; out of scope, noted on stash init scaffolds the EQL v2 authoring surface into customer repos, against a v3 database #733.
  • The empty-schema EncryptionV3({ schemas: [] }) throw is preserved — EncryptionV3 delegates to Encryption, so stash encrypt's "no schemas yet" error path is unchanged.

Verification

  • pnpm --filter stash test: 53 files, 638 tests passed
  • pnpm --filter "stash..." build: success
  • pnpm run code:check: 0 errors (pre-existing warnings only)
  • No test updates needed (build-schema.test.ts mocks the generator; utils.test.ts asserts nothing about the strings)
  • Changeset added ('stash': patch)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • stash init now scaffolds EQL v3 encryption clients compatible with the EQL v3 database schema.
    • Generated clients use EQL v3 domain types and APIs for text, numeric, JSON, and other encrypted fields.
    • Drizzle integrations now generate v3-compatible schema extraction and encryption client setup.
  • Documentation
    • Updated initialization guidance and examples to reflect EQL v3 patterns.

stash init installs an EQL v3 database but the client scaffolding still
emitted v2 authoring patterns (Encryption, encryptedColumn chains,
encryptedType flag objects), handing the customer's agent v2 guidance
against a v3 schema. Follow-up to #732.

- Rewrite DRIZZLE_PLACEHOLDER / GENERIC_PLACEHOLDER to EncryptionV3,
  the types.* concrete-domain factories, and the stack-drizzle/v3 entry.
- Port the live introspection generators (generateClientFromSchemas +
  generateDrizzleFromSchemas / generateGenericFromSchemas) to v3 via a
  new v3DomainFactory() mapping SearchOp sets to fixed-capability domains.
- Delete the dead singular generators (generateClientFromSchema,
  generateDrizzleFromSchema, generateSchemaFromDef, drizzleTsType) that
  had no callers rather than porting v2 dead code.

encryptionClient export shape and the empty-schema error path unchanged.
@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 04f5a13

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/prisma-next Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch

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

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

stash init and schema-based client generation now scaffold EQL v3 encryption clients. Column search operations map to fixed types.* domains, while Drizzle uses v3 schema extraction and all placeholders use EncryptionV3.

Changes

EQL v3 scaffolding

Layer / File(s) Summary
Domain capability mapping
packages/cli/src/commands/init/utils.ts
Maps column data types and search operations to concrete EQL v3 domain factories.
Generated client integration
packages/cli/src/commands/init/utils.ts
Updates Drizzle and generic multi-schema generators to use types.*, EncryptionV3, and extractEncryptionSchemaV3.
Placeholder templates and changeset
packages/cli/src/commands/init/utils.ts, .changeset/init-placeholder-eql-v3.md
Replaces EQL v2 examples and imports with EQL v3 patterns while preserving the client export shape and empty-schema error path.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: coderdan

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code updates placeholders and live generators to EQL v3 APIs, shared domain mapping, and boolean storage-only behavior as requested in #733.
Out of Scope Changes check ✅ Passed The changes stay focused on the v2-to-v3 scaffolding migration and related docs, with no obvious unrelated functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: updating stash init to scaffold an EQL v3 encryption client.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/init-placeholder-eql-v3

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Fixes a real drift: stash init installs an EQL v3 database (since #705), but the client scaffold it wrote into the customer's repo still taught v2 authoring (Encryption, encryptedColumn().equality().freeTextSearch(), encryptedType<T>('x', { equality: true })). Since that scaffold is what the customer's coding agent reads to do the real schema work, the scaffold and the database disagreed. This ports the two live surfaces in init/utils.ts — the DRIZZLE_PLACEHOLDER/GENERIC_PLACEHOLDER templates and generateClientFromSchemas — to the v3 surface (EncryptionV3, types.*, @cipherstash/stack-drizzle/v3).

Tight, well-scoped, and correct. LGTM / approvable — details below; I found no blocking issues.

What I verified

  • Every v3 API the scaffold now teaches is real. Confirmed against the branch: the types namespace (packages/stack/src/eql/v3/types.ts) exposes all 13 factory names the mapper emits (Text/TextEq/TextOrd/TextMatch/TextSearch, Integer/IntegerEq/IntegerOrd, Date/DateEq/DateOrd, Boolean, Json); @cipherstash/stack/v3 re-exports EncryptionV3 + encryptedTable + types (its own docstring shows the identical import the scaffold writes); and @cipherstash/stack-drizzle/v3 exports extractEncryptionSchemaV3 and a types namespace that is a typed 1:1 mirror (Object.fromEntries over the stack types, keyed [K in keyof V3Types]), so types.TextSearch(...) etc. resolve on the Drizzle side too. No broken import or unknown factory can be scaffolded.
  • v3DomainFactory is exhaustive and the capability mapping is right. DataType is exactly 'string' | 'number' | 'boolean' | 'date' | 'json', and the switch returns on every arm — no path yields undefined (which would emit types.undefined(...)). The capability→domain picks are correct: ord → *Ord (which also answers equality), eq → *Eq, else storage-only; for strings text && (eq||ord) → TextSearch, text → TextMatch. Matches the fixed-capability domains in eql/v3/columns.ts.
  • The deletions are safe. generateClientFromSchema (singular, exported), generateDrizzleFromSchema, generateSchemaFromDef, and drizzleTsType have zero callers anywhere in packages/, examples/, or e2e/ (grepped the branch) — stash schema build uses the plural generateClientFromSchemas. Removing unreachable v2 codegen rather than porting it to dead v3 codegen is the right call.
  • Changeset (stash patch) is correct for a CLI-only scaffold change; the EncryptionV3({ schemas: [] }) empty-schema throw path is preserved (delegates to Encryption), so stash encrypt's "no schemas yet" error is unchanged.

Notes — non-blocking, all acknowledged in the PR

  1. Numeric collapses to Integer*. A number column that's actually float/decimal scaffolds as IntegerOrd/IntegerEq/Integer — the wrong domain for float data. The introspected SearchOp model carries no width/precision signal (v2 collapsed to one dataType: 'number' too), and the template is explicitly "pattern reference, do NOT use as-is" with the user's real schema authoritative. Correctly deferred to #733. Fine for a scaffold.
  2. Boolean equality is silently dropped. v2 allowed equality on a boolean; v3 has no boolean eq/ord domain, so a boolean introspected with equality maps to storage-only Boolean. The generated code is valid v3 and the PR surfaces the difference — just worth being aware the scaffold quietly narrows that capability.
  3. Skills: this changes user-facing scaffold output, and AGENTS.md asks to check skills on user-facing workflow changes. The stash-cli/stash-encryption skills document commands rather than reproducing these placeholder strings, so nothing here looks stale — but worth a glance to confirm no skill snippet still shows the old Encryption/encryptedType init scaffold (the broader v2→v3 skill refresh is happening in sibling PRs).

Verdict

Correct and complete for its scope: the scaffold now teaches v3 APIs that all exist, the capability mapping matches the real domains, and the removed code is genuinely unreachable. The notes are non-blocking and already tracked. I'd approve this — say the word and I'll flip it to a formal approval.

})

const ${schemaVarName} = extractEncryptionSchema(${varName})`
const ${schemaVarName} = extractEncryptionSchemaV3(${varName})`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL I've been slowly removing the V3 suffixes and Claude keeps trying to add them back.

With V2 gone, this can just be named extractEncryptionSchema.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderdan I have definitely had the same conversation with Claude. 🙄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked and decided — see the "Naming — decided" section in #707: v3 takes the unsuffixed name and *V3 becomes a @deprecated alias. Drizzle is the explicit exception — a hard break, because collapsing the ./v3 subpath makes extractEncryptionSchemaV3extractEncryptionSchema collide with today's v2 root name. That's why this scaffold can't drop the suffix yet: it imports from @cipherstash/stack/v3 and @cipherstash/stack-drizzle/v3, which only export the suffixed names until the collapse lands. Blocked on the v2-removal prerequisites (#693, #657). Leaving open as a tracked follow-up rather than resolving here.

${tableDefs.join('\n\n')}

export const encryptionClient = await Encryption({
export const encryptionClient = await EncryptionV3({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Its clunky and forces the user to understand a version number that they shouldn't have to know about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked and decided — see the "Naming — decided" section in #707: v3 takes the unsuffixed name and *V3 becomes a @deprecated alias. Drizzle is the explicit exception — a hard break, because collapsing the ./v3 subpath makes extractEncryptionSchemaV3extractEncryptionSchema collide with today's v2 root name. That's why this scaffold can't drop the suffix yet: it imports from @cipherstash/stack/v3 and @cipherstash/stack-drizzle/v3, which only export the suffixed names until the collapse lands. Blocked on the v2-removal prerequisites (#693, #657). Leaving open as a tracked follow-up rather than resolving here.

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved but I have feelings about the naming :p

@tobyhede
tobyhede merged commit cddb182 into main Jul 21, 2026
10 checks passed
@tobyhede
tobyhede deleted the fix/init-placeholder-eql-v3 branch July 21, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stash init scaffolds the EQL v2 authoring surface into customer repos, against a v3 database

3 participants