Skip to content

Conversation

@calvinbrewer
Copy link
Contributor

@calvinbrewer calvinbrewer commented Oct 30, 2025

Change Summary:
Added: The Cipherstash Drizzle package along with its initial interface utilizing protect operators.
Enhanced: Enabled users to extend the Encrypted type, a custom type from Drizzle, allowing for the definition of custom protect schemas.

This update aims to improve data protection capabilities and flexibility when working with encrypted data types in Drizzle.

calvinbrewer and others added 8 commits November 3, 2025 20:16
- Create new @cipherstash/drizzle package with Protect.js operators
- Move Drizzle integration tests from protect package to drizzle package
- Add schema extraction utilities for automatic Protect.js schema generation
- Update documentation with new API using createProtectOperators
- Export schema types needed by Drizzle package
- Add encryptedType helper for Drizzle table definitions
- Implement LazyOperatorPromise class to support deferred encryption
- Update all operators (eq, ne, gt, gte, lt, lte, between, notBetween, like, ilike, notIlike) to return LazyOperatorPromise when encryption is needed
- Override protectOps.and() to batch collect and encrypt all operator values in a single createSearchTerms call
- Update TypeScript types to reflect operators can return Promise<SQL> | SQL
- Update documentation and tests to demonstrate batched and() usage pattern

This significantly improves performance when using multiple operators in a query by batching all encryption operations into a single call instead of multiple separate calls.
- Add type parameter examples to all encryptedType usage in schema definitions
- Add TIP box explaining importance of type parameters for type safety
- Update Select + Decrypt example to demonstrate type inference after decryption
- Update test file to use type parameters for consistency
- Remove outdated WARNING about TypeScript support work in progress
- Update TODO list to reflect current state

This ensures developers understand that encryptedType<T> maintains type safety throughout the encryption/decryption flow, with TypeScript knowing the correct types of decrypted data.
- Refactor LazyOperatorPromise to implement Promise<SQL> instead of extending it
- Add auto-execution logic so operators work when awaited directly
- Store encryption context (protectClient, defaultProtectTable, protectTableCache) in LazyOperatorPromise
- Implement then/catch/finally to delegate to internal promise
- Add _execute() method to handle encryption when awaited outside of and()
- Update all operator constructors to pass encryption context

This fixes the 'Promise resolve or reject function is not callable' error
that occurred when operators were used standalone (not in protectOps.and()).
Operators now work both standalone and batched in and() for maximum flexibility.
…ors in and()

- Add example showing regular Drizzle operators (eq, ne, gt, etc.) can be mixed with Protect operators in protectOps.and()
- Add note explaining that protectOps.and() automatically handles both encrypted and non-encrypted column operators
- Update test comments to clarify mixing of operator types
- Import eq from drizzle-orm in test file for clarity

The implementation already supported this capability - protectOps.and() correctly separates lazy operators (for encrypted columns) from regular SQL conditions (for non-encrypted columns) and combines them appropriately.
@auxesis auxesis changed the title Add Drizzle ORM integration guide" Add Drizzle ORM integration guide Nov 5, 2025
- Add workflow step to create .env file for drizzle package with database URL
- Add SQL script to create protect-ci table for integration tests
- Update postgres entrypoint to run CI table creation script
- Add SKIP_ORDER_BY_TEST flag to drizzle and supabase test suites
- CI database doesn't support order by on encrypted columns
- Skip order by tests gracefully with console log message
@calvinbrewer calvinbrewer changed the title Add Drizzle ORM integration guide Add Drizzle ORM integration Nov 5, 2025
@calvinbrewer calvinbrewer marked this pull request as ready for review November 5, 2025 03:04
Comment on lines +7 to +24
export type EncryptedColumnConfig = {
/**
* Data type for the column (default: 'string')
*/
dataType?: CastAs
/**
* Enable free text search. Can be a boolean for default options, or an object for custom configuration.
*/
freeTextSearch?: boolean | MatchIndexOpts
/**
* Enable equality index. Can be a boolean for default options, or an array of token filters.
*/
equality?: boolean | TokenFilter[]
/**
* Enable order and range index for sorting and range queries.
*/
orderAndRange?: boolean
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this type different to the ones used in protect itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those types are the same pulled from @cipherstash/schema package 🚀

Comment on lines +12 to +16
amount: encryptedType<number>('amount', {
dataType: 'number',
equality: true,
orderAndRange: true,
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a blocker but this is slightly duplicative. Given that the generic type is number, maybe the dataType can be inferred?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call - we can probably infer the dataType from the type interface provided to encrytpedType.

# Database connection
DATABASE_URL="postgresql://[username]:[password]@[host]:5432/[database]"

# CipherStash credentials
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we link to some docs or just the sign-up page for how to get these creds?

Comment on lines +108 to +119
# Get all transactions
curl http://localhost:3000/transactions

# Filter by account number
curl "http://localhost:3000/transactions?accountNumber=1234"

# Filter by amount range
curl "http://localhost:3000/transactions?minAmount=100&maxAmount=1000"

# Filter by status
curl "http://localhost:3000/transactions?status=completed"
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Fine for demo but maybe add a comment that params should usually be done via POST if they are sensitive so that they don't appear in logs.

isNull,
isNotNull,
not,
and: protectAnd,
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this up to above the comment.

})
```

> 💡 **Type Safety Tip**: Always specify the type parameter (`encryptedType<string>`, `encryptedType<number>`, etc.) to maintain type safety after decryption. Without it, decrypted values will be typed as `unknown`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth explaining in a note below that this is because the database doesn't know what type that was encrypted.

Suggested (outside of the quote):

This is because the database only sees encrypted data and doesn't know the underlying type. It must be specified in the ORM.

const results = await db
.select()
.from(usersTable)
.where(await protectOps.eq(usersTable.email, '[email protected]'))
Copy link
Contributor

Choose a reason for hiding this comment

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

It's slightly annoying that we need the await here. I wonder if the Drizzle team have any suggestions about how we could avoid it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it requires a call to ZeroKMS to create the encrypted search term we'd need for the core Drizzle interface to be aware of promises in the filter clauses

Copy link

Choose a reason for hiding this comment

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

Do the underlying FFI calls have to be async?
Would it be possible to expose an (optional) sync interface for some use cases?

const decrypted = await protectClient.bulkDecryptModels(results)
```

> ⚠️ **Note**: Sorting with ORE on Supabase and other databases that don't support operator families may not work as expected.
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorting won't work on Supabase. Period.

)
```

> 💡 **Performance Tip**: Using `protectOps.and()` batches all encryption operations into a single `createSearchTerms` call, which is more efficient than awaiting each operator individually.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

@@ -0,0 +1,426 @@
# Drizzle ORM Integration with Protect.js
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this doc superseded by the README? Seems like its saying the same stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah it's pretty much a duplicate. I'll just remove it and we can add better how tos later

@calvinbrewer calvinbrewer merged commit f583b60 into main Nov 6, 2025
1 check passed
@calvinbrewer calvinbrewer deleted the drizzle-orm branch November 6, 2025 14:42
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.

4 participants