feat(orm): add encrypted column decorator with deterministic and blind indexes#1143
feat(orm): add encrypted column decorator with deterministic and blind indexes#1143RomainLanz wants to merge 32 commits into22.xfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class support for encrypted Lucid columns via a new @column.encrypted(...) decorator, including deterministic and blind-index modes, plus query rewriting/validation and a pluggable encryption provider on BaseModel.
Changes:
- Introduces encrypted column metadata/types and
BaseModel.useEncryption()/$getEncryption()provider plumbing - Adds
@column.encrypted()decorator with standard/deterministic/blind modes and blind-index persistence - Rewrites equality-style query methods for encrypted-queryable columns and throws on unsupported operators/methods, with tests
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/orm/model_query_builder.spec.ts | Adds tests for deterministic/blind query rewriting and unsupported operator errors |
| test/orm/base_model.spec.ts | Adds tests for encrypt/decrypt, blind-index persistence, and missing-provider/config errors |
| src/types/model.ts | Introduces encryption-related public types and model API surface |
| src/orm/query_builder/index.ts | Implements encrypted-column query rewriting + unsupported-method enforcement |
| src/orm/main.ts | Exports the new encrypted decorator entrypoint |
| src/orm/decorators/index.ts | Wires column.encrypted into the decorators API |
| src/orm/decorators/encrypted.ts | Implements the encrypted column decorator and metadata validation |
| src/orm/base_model/index.ts | Adds encryption provider storage/inheritance and blind-index persistence |
| src/errors.ts | Adds dedicated encryption-related error types/messages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type ModelEncryptionContract = { | ||
| encrypt(value: any, options?: { deterministic?: boolean; [key: string]: any }): any | ||
| decrypt(value: any): any | ||
| blindIndexes(value: any, options?: { [key: string]: any }): any | ||
| blindIndex(value: any, options: { purpose: string; [key: string]: any }): any |
There was a problem hiding this comment.
Since this is a public contract type, the pervasive any weakens type safety for consumers and makes it harder to use correctly. Consider swapping to unknown (inputs/outputs) plus Record<string, unknown> for option bags, and/or making the contract generic over ciphertext/plaintext types to provide safer signatures without constraining implementations.
| export type ModelEncryptionContract = { | |
| encrypt(value: any, options?: { deterministic?: boolean; [key: string]: any }): any | |
| decrypt(value: any): any | |
| blindIndexes(value: any, options?: { [key: string]: any }): any | |
| blindIndex(value: any, options: { purpose: string; [key: string]: any }): any | |
| export type ModelEncryptionContract< | |
| Plaintext = unknown, | |
| Ciphertext = unknown, | |
| BlindIndex = unknown, | |
| BlindIndexMap = Record<string, unknown> | |
| > = { | |
| encrypt( | |
| value: Plaintext, | |
| options?: { deterministic?: boolean } & Record<string, unknown> | |
| ): Ciphertext | |
| decrypt(value: Ciphertext): Plaintext | |
| blindIndexes( | |
| value: Plaintext, | |
| options?: Record<string, unknown> | |
| ): BlindIndexMap | |
| blindIndex( | |
| value: Plaintext, | |
| options: { purpose: string } & Record<string, unknown> | |
| ): BlindIndex |
…ind index query rewriting
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ass prepareForAdapter
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
We have some flacky tests that should be fixed in another PR. I believe this one is ready for review. I hope all use cases have been handled! |
|
This pull request has been marked as stale because it has been inactive for more than 21 days. Please reopen if you still intend to submit this pull request |
|
This pull request has been automatically closed because it has been inactive for more than 4 weeks. Please reopen if you still intend to submit this pull request |
Hey there! 👋🏻
This PR adds encrypted columns to Lucid via a new
@column.encrypted(...)decorator with three modes: standard, deterministic, and blind index, plus per-column driver selection.@column.encrypted({ deterministic: true, driver })keeps encrypted persistence and enables equality queries by rewriting query values with deterministic encryption on the selected driver.@column.encrypted({ blind: { columnName, purpose }, driver })stores the main value with standard encryption and writes a blind index to another column, then rewrites equality queries to target that blind column.blind.purposeis required.blind.columnNameandblind.purposeare normalized (trim) when stored in metadata.Lucid relies on an injectable
ModelEncryptionContract(encrypt,decrypt,blindIndex,blindIndexes) bound viaBaseModel.useEncryption(...).If encrypted columns are used without an encryption provider, Lucid throws a dedicated error.
The encryption provider is dynamically inherited, so models booted before
BaseModel.useEncryption(...)still pick it up later.Query rewriting is implemented for equality-style WHERE methods on deterministic/blind encrypted columns:
where,orWhere,whereNot,orWhereNotwhereIn,orWhereIn,whereNotIn,orWhereNotInwhere({ email: 'x' })where('col', 'in', [...])/where('col', 'not in', [...])Blind indexes support both
blindIndex()andblindIndexes()(for key rotation). When multiple indexes are returned, clauses are automatically rewritten towhereIn/whereNotIn.Standard encrypted columns are explicitly rejected in
where*andwhereIn*with a dedicated error, instead of silently generating wrong queries.Unsupported operations now throw clear errors for encrypted columns, including non-equality and JSON operators (
whereLike,whereBetween,whereJson, etc.), column-to-column comparisons (whereColumn,orWhereColumn,whereNotColumn,orWhereNotColumn), ordering/grouping (orderBy,groupBy), and arithmetic updates (increment,decrement) including object payload forms.Write-path hardening was also added.
User.query().where(...).update({...})and key/valueupdatepaths now apply encryption consistently, and blind-index persistence on update is stable regardless of object key order (computed blind values always win over manually provided blind columns).