Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
735c958
feat(orm): add encrypted column decorator with deterministic and blin…
RomainLanz Feb 7, 2026
8fcfc7b
feat(orm): add encrypted column decorators with driver support and bl…
RomainLanz Feb 7, 2026
a1e0adc
fix(orm): encrypt ModelQueryBuilder update payloads and sync blind in…
RomainLanz Feb 7, 2026
3de2e12
fix(orm): avoid encrypting joined table columns with matching names
RomainLanz Feb 7, 2026
1959601
fix(orm): harden encrypted column query/write paths and blind-index h…
RomainLanz Feb 7, 2026
832f316
test(db): include users_encrypted in table snapshots and shared test …
RomainLanz Feb 7, 2026
4d10090
test(db): remove table name
RomainLanz Feb 7, 2026
5c97073
fix(orm): support in/not in operators for encrypted where clauses
RomainLanz Feb 7, 2026
9a1a77f
fix(orm): validate blind encrypted metadata in model write path
RomainLanz Feb 7, 2026
122f944
test(orm): cover unsupported encrypted whereBetween and whereJson ope…
RomainLanz Feb 7, 2026
aea389b
fix(orm): harden encrypted where rewriting (in/not in, object, and/or)
RomainLanz Feb 7, 2026
881dfc5
refactor(orm): optimize encrypted update payload preparation in Model…
RomainLanz Feb 7, 2026
b80919b
fix(encrypted): exclude driver from column options passed to $addColumn
RomainLanz Feb 7, 2026
10a37e0
refactor(query-builder): deduplicate encrypted where and whereIn logic
RomainLanz Feb 7, 2026
d68f680
fix(query-builder): rewrite object where variants for blind multi-ind…
RomainLanz Feb 7, 2026
52f202e
fix(query-builder): guard whereColumn comparisons on encrypted columns
RomainLanz Feb 7, 2026
79cc5a0
refactor(base-model): compute blind index writes in deferred single-p…
RomainLanz Feb 7, 2026
58712d7
refactor(query-builder): validate qualified source before encrypted c…
RomainLanz Feb 7, 2026
9ad4551
fix(encrypted-column): pass driver to decrypt during consume
RomainLanz Feb 7, 2026
90fe535
types(encrypted-column): narrow allowed column options
RomainLanz Feb 7, 2026
8aa33b0
test(query-builder): cover encrypted key/value update with returning
RomainLanz Feb 7, 2026
f0dc615
fix(query-builder): reject increment/decrement on encrypted columns
RomainLanz Feb 7, 2026
131629a
test(schema): index blind column on users_encrypted table
RomainLanz Feb 7, 2026
54ea6ce
refactor(query-builder): use named options for encrypted method guards
RomainLanz Feb 7, 2026
79a5a37
fix(query-builder): block object-form increment/decrement on encrypte…
RomainLanz Feb 7, 2026
19a7cb6
fix(base-model): keep encryption provider dynamically inherited after…
RomainLanz Feb 7, 2026
51c3bd0
fix(query-builder): use explicit error for standard encrypted where q…
RomainLanz Feb 7, 2026
3e2df46
fix(encrypted-column): keep driver value as provided and trim only bl…
RomainLanz Feb 7, 2026
2e3d540
chore(errors): reword unsupported encrypted query message for non-whe…
RomainLanz Feb 7, 2026
5d5eca4
refactor(query-builder): simplify encrypted method guard signature
RomainLanz Feb 7, 2026
44fa650
refactor(query-builder): extract encrypted query support into dedicat…
RomainLanz Feb 7, 2026
ec11ddb
refactor(encryption): replace useEncryption API with config defaults …
RomainLanz Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ export const E_MODEL_DELETED = createError(
500
)

export const E_MISSING_MODEL_ENCRYPTION = createError<[string]>(
'Cannot use encrypted column "%s" without a configured encryption provider. Call "BaseModel.useEncryption()" before querying or persisting encrypted columns',
'E_MISSING_MODEL_ENCRYPTION',
500
)

export const E_INVALID_ENCRYPTED_COLUMN_CONFIGURATION = createError<[string, string]>(
'Invalid encrypted column configuration for "%s". %s',
'E_INVALID_ENCRYPTED_COLUMN_CONFIGURATION',
500
)

export const E_UNSUPPORTED_ENCRYPTED_COLUMN_QUERY = createError<[string, string]>(
'Cannot use "%s" on encrypted column "%s". Only equality-based queries are supported',
Comment thread
RomainLanz marked this conversation as resolved.
Outdated
'E_UNSUPPORTED_ENCRYPTED_COLUMN_QUERY',
500
)

/**
* The "E_ROW_NOT_FOUND" exception is raised when
* no row is found in a database single query
Expand Down
45 changes: 45 additions & 0 deletions src/orm/base_model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type ModelKeysContract,
type ModelAssignOptions,
type ModelAdapterOptions,
type ModelEncryptionContract,
type ModelRelationOptions,
type ModelQueryBuilderContract,
type ModelPaginatorContract,
Expand Down Expand Up @@ -112,6 +113,11 @@ class BaseModelImpl implements LucidRow {
*/
static $adapter: AdapterContract

/**
* Encryption provider used by encrypted columns.
*/
static $encryption?: ModelEncryptionContract

/**
* Define an adapter to use for interacting with
* the database
Expand All @@ -120,6 +126,27 @@ class BaseModelImpl implements LucidRow {
this.$adapter = adapter
}

/**
* Define encryption provider to use for encrypted columns.
*/
static useEncryption(encryption: ModelEncryptionContract) {
Comment thread
RomainLanz marked this conversation as resolved.
Outdated
this.$encryption = encryption
}

/**
* Returns encryption provider and raises when missing.
*/
static $getEncryption(attributeName?: string): ModelEncryptionContract {
this.boot()

if (this.$encryption) {
return this.$encryption
}

const dottedAttribute = attributeName ? `${this.name}.${attributeName}` : this.name
throw new errors.E_MISSING_MODEL_ENCRYPTION([dottedAttribute])
}

/**
* Naming strategy for model properties
*/
Expand Down Expand Up @@ -647,6 +674,11 @@ class BaseModelImpl implements LucidRow {
*/
this.$defineProperty('selfAssignPrimaryKey', false, 'inherit')

/**
* Inherit encryption provider.
*/
this.$defineProperty('$encryption', undefined, 'inherit')

/**
* Define the keys' property. This allows looking up variations
* for model keys
Expand Down Expand Up @@ -1357,13 +1389,26 @@ class BaseModelImpl implements LucidRow {

return Object.keys(attributes).reduce((result: any, key) => {
const column = Model.$getColumn(key)!
const encryption = column.meta?.encryption

const value =
typeof column.prepare === 'function'
? column.prepare(attributes[key], key, this)
: attributes[key]

result[column.columnName] = value

if (encryption?.mode === 'blind') {
const blindColumnName = encryption.blindColumnName as string
const purpose = encryption.purpose as string
const encryptionProvider = Model.$getEncryption(key)

result[blindColumnName] =
attributes[key] === null || attributes[key] === undefined
? attributes[key]
: encryptionProvider.blindIndex(attributes[key], { purpose })
}
Comment thread
RomainLanz marked this conversation as resolved.
Outdated

return result
}, {})
}
Expand Down
109 changes: 109 additions & 0 deletions src/orm/decorators/encrypted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* @adonisjs/lucid
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import * as errors from '../../errors.js'
import {
type LucidRow,
type LucidModel,
type EncryptedColumnMeta,
type EncryptedColumnOptions,
type EncryptedColumnDecorator,
} from '../../types/model.js'

function defineEncryptedMeta(
model: LucidModel,
property: string,
options?: EncryptedColumnOptions
): EncryptedColumnMeta {
const dottedAttribute = `${model.name}.${property}`

if (options?.deterministic && options?.blind) {
throw new errors.E_INVALID_ENCRYPTED_COLUMN_CONFIGURATION([
dottedAttribute,
'The "deterministic" and "blind" options cannot be used together',
])
}

if (options?.blind) {
if (!options.blind.columnName?.trim()) {
throw new errors.E_INVALID_ENCRYPTED_COLUMN_CONFIGURATION([
dottedAttribute,
'Missing "blind.columnName"',
])
}

if (!options.blind.purpose?.trim()) {
throw new errors.E_INVALID_ENCRYPTED_COLUMN_CONFIGURATION([
dottedAttribute,
'Missing "blind.purpose"',
])
}

return {
mode: 'blind',
blindColumnName: options.blind.columnName,
purpose: options.blind.purpose,
}
}

if (options?.deterministic) {
return {
mode: 'deterministic',
}
}

return {
mode: 'standard',
}
}

/**
* Decorator to define an encrypted column
*/
export const encryptedColumn: EncryptedColumnDecorator = (options?) => {
return function decorateAsEncryptedColumn(target, property) {
const Model = target.constructor as LucidModel
Model.boot()

const {
deterministic: droppedDeterministic,
blind: droppedBlind,
...columnOptions
} = options || {}
void droppedDeterministic
void droppedBlind
Comment thread
RomainLanz marked this conversation as resolved.
Outdated
Comment thread
RomainLanz marked this conversation as resolved.
Outdated
const encryptionMeta = defineEncryptedMeta(Model, property, options)
const meta = Object.assign({}, columnOptions.meta, { encryption: encryptionMeta })

Model.$addColumn(property, {
...columnOptions,
meta,
prepare(value: any, attributeName: string, modelInstance: LucidRow) {
if (value === null || value === undefined) {
return value
}

const encryption = (modelInstance.constructor as LucidModel).$getEncryption(attributeName)
if (encryptionMeta.mode === 'deterministic') {
return encryption.encrypt(value, { deterministic: true })
}

return encryption.encrypt(value)
},
consume(value: any, attributeName: string, modelInstance: LucidRow) {
if (value === null || value === undefined) {
return value
}

const encryption = (modelInstance.constructor as LucidModel).$getEncryption(attributeName)
return encryption.decrypt(value)
},
})
}
}
4 changes: 4 additions & 0 deletions src/orm/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type LucidModel,
type HooksDecorator,
type ColumnDecorator,
type EncryptedColumnDecorator,
type ComputedDecorator,
type DateColumnDecorator,
type DateTimeColumnDecorator,
Expand All @@ -26,6 +27,7 @@ import {

import { dateColumn } from './date.js'
import { dateTimeColumn } from './date_time.js'
import { encryptedColumn } from './encrypted.js'

/**
* Define property on a model as a column. The decorator needs a
Expand All @@ -34,6 +36,7 @@ import { dateTimeColumn } from './date_time.js'
export const column: ColumnDecorator & {
date: DateColumnDecorator
dateTime: DateTimeColumnDecorator
encrypted: EncryptedColumnDecorator
} = (options?) => {
return function decorateAsColumn(target, property) {
const Model = target.constructor as LucidModel
Expand All @@ -44,6 +47,7 @@ export const column: ColumnDecorator & {

column.date = dateColumn
column.dateTime = dateTimeColumn
column.encrypted = encryptedColumn

/**
* Define computed property on a model. The decorator needs a
Expand Down
1 change: 1 addition & 0 deletions src/orm/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export * from './decorators/index.js'
export * from './decorators/date.js'
export * from './decorators/date_time.js'
export * from './decorators/encrypted.js'
export { BaseModel, scope } from './base_model/index.js'
export { ModelQueryBuilder } from './query_builder/index.js'
export { ModelPaginator } from './paginator/index.js'
Expand Down
Loading
Loading