feat(core): add @EncryptedAttribute decorator for transparent column encryption by nullxx · Pull Request #18157 · sequelize/sequelize · GitHub
Skip to content

feat(core): add @EncryptedAttribute decorator for transparent column encryption#18157

Open
nullxx wants to merge 2 commits intosequelize:mainfrom
nullxx:feat/encrypted-attribute-decorator
Open

feat(core): add @EncryptedAttribute decorator for transparent column encryption#18157
nullxx wants to merge 2 commits intosequelize:mainfrom
nullxx:feat/encrypted-attribute-decorator

Conversation

@nullxx
Copy link
Copy Markdown

@nullxx nullxx commented Mar 13, 2026

Pull Request Checklist

  • Have you added new tests to prevent regressions?
  • If a documentation update is necessary, have you opened a PR to the documentation repository?
  • Did you update the typescript typings accordingly (if applicable)?
  • Does the description below contain a link to an existing issue (Closes #[issue]) or a description of the issue you are solving?
  • Does the name of your PR follow our conventions?

Description of Changes

Closes #17299

Summary

Adds a new @EncryptedAttribute decorator to @sequelize/core/decorators-legacy that provides transparent at-rest encryption for model attributes. The attribute is stored in the database as a BLOB containing a versioned packed ciphertext and is automatically decrypted on read, with the value coerced back to the declared logical DataType.

Motivation

Sequelize currently has no built-in mechanism for encrypting sensitive model fields (SSNs, credit card numbers, PII, API keys, etc.). Users must manually implement encryption in hooks or getters/setters, which is error-prone and inconsistent. This decorator provides a standardised, secure-by-default solution that integrates natively with Sequelize's decorator infrastructure.

Design decisions

Strategy Pattern for cipher extensibility -- Instead of hardcoding a single algorithm, the decorator accepts a CipherStrategy interface. Two built-in strategies are provided:

  • Aes256GcmStrategy (default) -- AES-256-GCM with random 12-byte IV + 16-byte auth tag. Provides both confidentiality and authenticity.
  • Aes256CbcStrategy -- AES-256-CBC with random 16-byte IV. For environments where GCM is not available.

Users can implement their own CipherStrategy (e.g. chacha20-poly1305, envelope encryption with KMS, etc.) without any changes to Sequelize.

Versioned ciphertext format -- Every packed ciphertext is prefixed with a version byte (0x01), allowing the format to evolve in future versions without breaking existing encrypted data.

Random IV per encryption -- Every set operation generates a fresh random IV via crypto.randomBytes(). This is critical for GCM security (IV reuse with the same key completely breaks GCM confidentiality and authenticity).

Native Sequelize integration via get/set -- Uses registerModelAttributeOptions() with custom get/set functions instead of global model hooks. This ensures multiple @EncryptedAttribute decorators on the same model work independently without conflicts.

Strict key validation -- Keys must be exactly 32 bytes (Buffer) or a 64-character hex string. The decorator throws eagerly at decoration time, not at runtime.

Usage example

import { Model, DataTypes, InferAttributes, InferCreationAttributes } from '@sequelize/core';
import { EncryptedAttribute, Aes256CbcStrategy } from '@sequelize/core/decorators-legacy';

class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  @EncryptedAttribute({
    type: DataTypes.STRING,
    key: process.env.ENCRYPTION_KEY!, // 64-char hex string
  })
  declare ssn: string;

  @EncryptedAttribute({
    type: DataTypes.JSON,
    key: process.env.ENCRYPTION_KEY!,
    strategy: Aes256CbcStrategy,
    blobLength: 'medium',
  })
  declare metadata: Record<string, unknown>;
}

Files changed

File Change
packages/core/src/decorators/legacy/encrypted-attribute.ts New file -- decorator, strategies, serialisation
packages/core/src/decorators/legacy/index.ts Added export * from './encrypted-attribute.js'
packages/core/test/unit/decorators/encrypted-attribute.test.ts New file -- 22 unit tests

Test coverage

  • Attribute registration as BLOB
  • Key validation (Buffer length, hex format, rejection of invalid keys)
  • Type validation (rejects non-DataType values)
  • Round-trip encrypt/decrypt for STRING, INTEGER, BOOLEAN, JSON, DATE
  • Null and undefined handling
  • Multiple encrypted attributes on same model (no conflicts)
  • Non-deterministic ciphertext verification (random IV)
  • Both Aes256GcmStrategy and Aes256CbcStrategy
  • Custom CipherStrategy support
  • blobLength option respected
  • Decorator validation (static property, non-Model class rejection)
  • Standalone strategy tests (round-trip, version byte, tamper detection, wrong key)

List of Breaking Changes

None. This is a purely additive feature with no changes to existing APIs.

Summary by CodeRabbit

  • New Features

    • Transparent attribute encryption with automatic encrypt/decrypt at rest
    • Multiple algorithms supported (AES-256-GCM, AES-256-CBC) and pluggable custom strategies
    • Configurable key formats, deterministic option, versioned ciphertext format, nullable handling, and configurable storage size
    • Support for multiple encrypted attributes per model
  • Tests

    • Comprehensive test suite covering encryption/decryption, strategy behavior, edge cases, and validation

…encryption

Add a new @EncryptedAttribute decorator that provides transparent at-rest
encryption for model attributes using a pluggable CipherStrategy interface.

- Aes256GcmStrategy (default): AES-256-GCM with random IV + auth tag
- Aes256CbcStrategy: AES-256-CBC with random IV
- Versioned ciphertext format (version byte prefix) for future migration
- Uses registerModelAttributeOptions with get/set (no global hooks)
- Supports multiple encrypted attributes per model without conflicts
- Strict key validation (32-byte Buffer or 64-char hex string)
- 22 unit tests covering round-trip, strategies, validation, and edge cases
@nullxx nullxx requested a review from a team as a code owner March 13, 2026 14:24
@nullxx nullxx requested review from ephys and sdepold March 13, 2026 14:24
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77dd6a7d-98ef-4793-a7f3-b527e602fd28

📥 Commits

Reviewing files that changed from the base of the PR and between 7452e9e and 58dd63f.

📒 Files selected for processing (2)
  • packages/core/src/decorators/legacy/encrypted-attribute.ts
  • packages/core/test/unit/decorators/encrypted-attribute.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/decorators/legacy/encrypted-attribute.ts

📝 Walkthrough

Walkthrough

Adds a legacy @EncryptedAttribute decorator with pluggable cipher strategies, serialization/deserialization helpers, key normalization, and versioned ciphertext handling; stores encrypted data as BLOB and integrates attribute metadata with Sequelize.

Changes

Cohort / File(s) Summary
Encrypted Attribute Decorator
packages/core/src/decorators/legacy/encrypted-attribute.ts
New decorator implementation: exports EncryptedAttribute, EncryptedAttributeOptions, CipherStrategy, EncryptionResult, Aes256GcmStrategy, Aes256CbcStrategy. Handles key normalisation, type (de)serialisers, packing/unpacking versioned ciphertext, deterministic flag, null handling, and BLOB storage semantics.
Public Export
packages/core/src/decorators/legacy/index.ts
Re-exports the new encrypted-attribute module to expose it as part of the legacy decorators public API.
Tests
packages/core/test/unit/decorators/encrypted-attribute.test.ts
Comprehensive unit tests covering decorator registration/validation, key validation, type validation, encryption/decryption round-trips across multiple DataTypes, strategy selection (including custom strategy), deterministic vs non-deterministic behaviour, BLOB sizing, and tamper detection.

Sequence Diagram(s)

sequenceDiagram
    participant User as User Code
    participant Model as Model Instance
    participant Decorator as EncryptedAttribute<br/>(getter/setter)
    participant Serializer as Serialization<br/>System
    participant Strategy as CipherStrategy
    participant DB as Database

    rect rgba(100,200,100,0.5)
    Note over User,DB: SET Operation (Encrypt)
    User->>Model: set attribute = value
    Model->>Decorator: setter(value)
    Decorator->>Serializer: serialize(value, dataType)
    Serializer-->>Decorator: plaintext Buffer
    Decorator->>Strategy: encrypt(plaintext, key)
    Strategy-->>Decorator: EncryptionResult { data }
    Decorator->>DB: store encrypted BLOB
    end

    rect rgba(100,150,200,0.5)
    Note over User,DB: GET Operation (Decrypt)
    User->>Model: read attribute
    Model->>Decorator: getter()
    Decorator->>DB: fetch encrypted BLOB
    DB-->>Decorator: packed ciphertext
    Decorator->>Strategy: decrypt(packed, key)
    Strategy-->>Decorator: plaintext Buffer
    Decorator->>Serializer: deserialize(plaintext, dataType)
    Serializer-->>Decorator: value
    Decorator-->>Model: return value
    Model-->>User: value
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 In burrows deep where secrets sleep,
I wrap them snug and fold them neat.
AES hops, IVs twirl and play,
Plaintext in, ciphertext away.
A rabbit's charm keeps data safe today.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the primary change: adding a new @EncryptedAttribute decorator for transparent encryption of Sequelize model columns.
Linked Issues check ✅ Passed The PR successfully implements all core objectives from issue #17299: decorator-based encryption API, transparent decryption on retrieval, Sequelize integration, secure standardized implementation with built-in strategies (Aes256GcmStrategy, Aes256CbcStrategy), extensible CipherStrategy interface, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to the @EncryptedAttribute decorator implementation and its integration. The file changes include the decorator implementation, re-export in the legacy decorators index, and comprehensive unit tests—all within scope of the linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.16.4)
packages/core/src/decorators/legacy/encrypted-attribute.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m

packages/core/test/unit/decorators/encrypted-attribute.test.ts

┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m


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 and usage tips.

@nullxx nullxx changed the title feat(core): add @EncryptedAttribute decorator for transparent column … feat(core): add @EncryptedAttribute decorator for transparent column encryption Mar 13, 2026
- Fix prettier formatting
- Add missing JSDoc @param declarations on serialise() and getDataTypeId()
- Replace for-loop with Buffer.map in test XOR strategy
- Remove unused InferCreationAttributes import
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/decorators/legacy/encrypted-attribute.ts`:
- Around line 181-182: Add missing JSDoc `@param` and `@returns` for the functions
serialise(value: unknown): Buffer and getDataTypeId(value: unknown): number so
the linter rule jsdoc/require-param is satisfied; update the comment blocks
above serialise and getDataTypeId to describe the parameter "value" and the
return type (Buffer for serialise, numeric data-type id for getDataTypeId) and
include a short description for each function. Ensure the param name matches the
function signature ("value") and the `@returns` describes the returned value to
satisfy the linter.
- Around line 452-456: The custom getter is called without injecting the
decrypted value into the instance, so userGet (called in the encrypted-attribute
decorator) still reads ciphertext via getDataValue(property); before calling
userGet.call(this) set the instance's data for that property to the decrypted
value (e.g., this.setDataValue(property, value)), then call userGet and
afterwards restore the original encrypted value to avoid mutating state
permanently; reference the symbols userGet, value, property, getDataValue and
setDataValue when making the change.
- Around line 467-470: The setter currently accepts null/undefined and persists
null unconditionally, bypassing the decorator's allowNull policy; update the
setter in encrypted-attribute.ts (the function that calls
this.setDataValue(propertyName, ...)) to check the decorator option (e.g.,
options.allowNull or the decorator's allowNull flag) when value == null: if
allowNull is true, continue to call this.setDataValue(propertyName as any, null
as any); otherwise throw a descriptive TypeError (or ValidationError)
immediately to enforce the decorator-level contract instead of persisting null.
- Around line 239-241: Update the deserializer branch that checks logicalType so
DATE remains mapped to a Date (return (buf: Buffer) => new
Date(buf.toString())), but change DATEONLY and TIME to return strings (return
(buf: Buffer) => buf.toString()). Locate the conditional that uses logicalType
=== 'DATE' || logicalType === 'DATEONLY' || logicalType === 'TIME' and split it
so 'DATE' uses new Date(...) while 'DATEONLY' and 'TIME' use buf.toString();
keep the same function/closure shape that accepts a Buffer and returns the
correctly typed value.

In `@packages/core/test/unit/decorators/encrypted-attribute.test.ts`:
- Line 2: The import statement currently brings in both InferAttributes and
InferCreationAttributes but InferCreationAttributes is unused and triggers the
linter; update the import in the test by removing the unused symbol
InferCreationAttributes so only InferAttributes is imported (i.e., change the
import that currently lists "InferAttributes, InferCreationAttributes" to only
include "InferAttributes").
- Around line 16-352: The suite mutates a shared Sequelize instance and model
registry; add test lifecycle hooks to isolate each test by creating/resetting
and tearing down the Sequelize instance: add a beforeEach that initializes a
fresh sequelize, calls sequelize.sync({ force: true }) and ensures any models
used by the tests are (re)registered via sequelize.addModels where needed, and
add an afterEach that closes the sequelize (sequelize.close()) and clears
registered models to avoid cross-test leakage; update the top-level describe
block in encrypted-attribute.test.ts to include these beforeEach/afterEach hooks
so tests using sequelize, sequelize.addModels, and the
EncryptedAttribute-decorated Model classes run against a clean DB instance each
time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09d94387-ba9d-436b-8762-e7f504a25bce

📥 Commits

Reviewing files that changed from the base of the PR and between 81a2032 and 7452e9e.

📒 Files selected for processing (3)
  • packages/core/src/decorators/legacy/encrypted-attribute.ts
  • packages/core/src/decorators/legacy/index.ts
  • packages/core/test/unit/decorators/encrypted-attribute.test.ts

Comment thread packages/core/src/decorators/legacy/encrypted-attribute.ts Outdated
Comment on lines +239 to +241
if (logicalType === 'DATE' || logicalType === 'DATEONLY' || logicalType === 'TIME') {
return (buf: Buffer) => new Date(buf.toString());
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify expected runtime types for DATEONLY/TIME in Sequelize data type definitions.
rg -n "class DATEONLY|class TIME|DATEONLY|TIME|AcceptedDate" packages/core/src/abstract-dialect/data-types.ts -C4

Repository: sequelize/sequelize

Length of output: 1790


🌐 Web query:

Sequelize DATEONLY TIME data type deserialization canonical types

💡 Result:

Canonical JS types (what you should expect in Sequelize)

  • DataTypes.DATEONLY is canonically a string in YYYY-MM-DD form (not a Date). Internally, Sequelize normalizes/binds values by formatting them with UTC semantics (dayjs.utc(...).format('YYYY-MM-DD')) when you set the attribute / send it to the DB. [1]
  • DataTypes.TIME is canonically a string containing a time-of-day (typically HH:mm:ss and possibly fractional seconds depending on dialect + precision). Sequelize mainly defines the SQL type/precision; it does not (in core) coerce it into a JS Date. [2]

Deserialization (DB → JS) behavior

  • When Sequelize reads values back, the value first may be parsed at the dialect level (via a dialect-registered parser), and then—if the query/attribute has an explicit Sequelize DataType—Sequelize calls the DataType’s parseDatabaseValue. This behavior is described in Sequelize’s “Extending Data Types” docs. [3]
  • DATEONLY’s class overrides sanitize / toBindableValue (for normalization & binding), but it does not override parseDatabaseValue in core—so what you get back is typically whatever the driver/dialect parser returns (commonly already a string). [1][3]
  • A common “gotcha” is timezone shifting in some dialect/driver combinations (notably MSSQL historically), where a stored DATE like 2019-06-01 could be returned as 2019-05-31 due to unintended UTC/local conversion. [4]

Practical takeaway

  • If you want date-only / time-only values with stable semantics across timezones, treat them as opaque strings (YYYY-MM-DD, HH:mm:ss[.SSS]) at your application boundary, and (if needed) use a dialect type parser / custom datatype parsing hook to force that behavior consistently. [3][4]

Sources:
[1] Sequelize v7 DATEONLY implementation (UTC formatting, string normalization) https://raw.githubusercontent.com/sequelize/sequelize/7a2d9317988d1ac5a80279687e75c35baf373526/packages/core/src/abstract-dialect/data-types.ts (snippet shown in search result)
[2] Sequelize v7 TIME datatype definition (precision + SQL type) https://raw.githubusercontent.com/sequelize/sequelize/7a2d9317988d1ac5a80279687e75c35baf373526/packages/core/src/abstract-dialect/data-types.ts (snippet shown in search result)
[3] Sequelize docs: Extending / custom data types (parseDatabaseValue, dialect parser notes) https://sequelize.org/docs/v7/other-topics/extending-data-types/
[4] Sequelize issue: MSSQL DATEONLY timezone conversion bug report #10982


🌐 Web query:

Sequelize DATEONLY TIME expected return type string or date

💡 Result:

In Sequelize, the expected JS return type differs by datatype:

  • DataTypes.DATEONLY: string, formatted as 'YYYY-MM-DD' (date-only, no time). (sequelize.org)

    • Sequelize may accept various “date-like” inputs when setting the value, but when you read it back it’s intended to be the normalized date string. (sequelize.org)
  • DataTypes.TIME: string (a time-only value; format is dialect/driver dependent but conceptually ISO-8601 time). (sequelize.org)

If you’re seeing a Date object (or an ISO datetime like 1970-01-01T...Z) for a TIME column, that’s typically coming from the dialect driver / parsing behavior rather than Sequelize’s intended TIME => string mapping. (sequelize.org)

Citations:


Map DATEONLY and TIME to strings instead of Date objects.

The canonical types in Sequelize are:

  • DATEDate object
  • DATEONLY → string (YYYY-MM-DD)
  • TIME → string (time-of-day)

Currently, all three are mapped to new Date(...), which breaks round-trips. Specifically, DATEONLY and TIME should deserialize to strings via buf.toString().

Proposed fix
-  if (logicalType === 'DATE' || logicalType === 'DATEONLY' || logicalType === 'TIME') {
+  if (logicalType === 'DATE') {
     return (buf: Buffer) => new Date(buf.toString());
   }
+
+  if (logicalType === 'DATEONLY' || logicalType === 'TIME') {
+    return (buf: Buffer) => buf.toString();
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (logicalType === 'DATE' || logicalType === 'DATEONLY' || logicalType === 'TIME') {
return (buf: Buffer) => new Date(buf.toString());
}
if (logicalType === 'DATE') {
return (buf: Buffer) => new Date(buf.toString());
}
if (logicalType === 'DATEONLY' || logicalType === 'TIME') {
return (buf: Buffer) => buf.toString();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/decorators/legacy/encrypted-attribute.ts` around lines 239
- 241, Update the deserializer branch that checks logicalType so DATE remains
mapped to a Date (return (buf: Buffer) => new Date(buf.toString())), but change
DATEONLY and TIME to return strings (return (buf: Buffer) => buf.toString()).
Locate the conditional that uses logicalType === 'DATE' || logicalType ===
'DATEONLY' || logicalType === 'TIME' and split it so 'DATE' uses new Date(...)
while 'DATEONLY' and 'TIME' use buf.toString(); keep the same function/closure
shape that accepts a Buffer and returns the correctly typed value.

Comment on lines +452 to +456
// If the user provided a custom getter, call it with the decrypted value
// already set so getDataValue returns the decrypted form.
if (userGet) {
return userGet.call(this);
}
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.

⚠️ Potential issue | 🟠 Major

Custom getter receives ciphertext instead of decrypted value.

Lines 452-456 state decrypted data is exposed to userGet, but value is never injected before calling userGet.call(this). A custom getter using getDataValue(property) will still read encrypted bytes.

💡 Proposed fix
         const decrypted = strategy.decrypt(raw, keyBuf);
         const value = deserialise(decrypted);
@@
         if (userGet) {
-          return userGet.call(this);
+          const dataValues = (this as any).dataValues;
+          const previous = dataValues[propertyName];
+          dataValues[propertyName] = value;
+          try {
+            return userGet.call(this);
+          } finally {
+            dataValues[propertyName] = previous;
+          }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If the user provided a custom getter, call it with the decrypted value
// already set so getDataValue returns the decrypted form.
if (userGet) {
return userGet.call(this);
}
// If the user provided a custom getter, call it with the decrypted value
// already set so getDataValue returns the decrypted form.
if (userGet) {
const dataValues = (this as any).dataValues;
const previous = dataValues[propertyName];
dataValues[propertyName] = value;
try {
return userGet.call(this);
} finally {
dataValues[propertyName] = previous;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/decorators/legacy/encrypted-attribute.ts` around lines 452
- 456, The custom getter is called without injecting the decrypted value into
the instance, so userGet (called in the encrypted-attribute decorator) still
reads ciphertext via getDataValue(property); before calling userGet.call(this)
set the instance's data for that property to the decrypted value (e.g.,
this.setDataValue(property, value)), then call userGet and afterwards restore
the original encrypted value to avoid mutating state permanently; reference the
symbols userGet, value, property, getDataValue and setDataValue when making the
change.

Comment on lines +467 to +470
if (value == null) {
this.setDataValue(propertyName as any, null as any);

return;
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.

⚠️ Potential issue | 🟡 Minor

allowNull is not enforced in the setter.

Line 467 accepts null/undefined and persists null regardless of allowNull, which bypasses decorator-level intent and delays failure to later validation/database layers.

💡 Proposed fix
         if (value == null) {
+          if (!allowNull) {
+            throw new TypeError(`@EncryptedAttribute: "${propertyName}" does not allow null values.`);
+          }
           this.setDataValue(propertyName as any, null as any);
 
           return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (value == null) {
this.setDataValue(propertyName as any, null as any);
return;
if (value == null) {
if (!allowNull) {
throw new TypeError(`@EncryptedAttribute: "${propertyName}" does not allow null values.`);
}
this.setDataValue(propertyName as any, null as any);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/decorators/legacy/encrypted-attribute.ts` around lines 467
- 470, The setter currently accepts null/undefined and persists null
unconditionally, bypassing the decorator's allowNull policy; update the setter
in encrypted-attribute.ts (the function that calls
this.setDataValue(propertyName, ...)) to check the decorator option (e.g.,
options.allowNull or the decorator's allowNull flag) when value == null: if
allowNull is true, continue to call this.setDataValue(propertyName as any, null
as any); otherwise throw a descriptive TypeError (or ValidationError)
immediately to enforce the decorator-level contract instead of persisting null.

Comment thread packages/core/test/unit/decorators/encrypted-attribute.test.ts Outdated
Comment on lines +16 to +352
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.

⚠️ Potential issue | 🟠 Major

Add test lifecycle hooks for isolation and guideline compliance.

This suite mutates shared Sequelize model registration without beforeEach/afterEach setup/teardown, which can cause inter-test coupling and violates the repository test lifecycle rule.

As per coding guidelines "Use beforeEach/afterEach to sync with { force: true } and close Sequelize instances in tests".

🧰 Tools
🪛 ESLint

[error] 277-277: Use a for-of loop instead of this for loop.

(unicorn/no-for-loop)


[error] 285-285: Use a for-of loop instead of this for loop.

(unicorn/no-for-loop)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/test/unit/decorators/encrypted-attribute.test.ts` around lines
16 - 352, The suite mutates a shared Sequelize instance and model registry; add
test lifecycle hooks to isolate each test by creating/resetting and tearing down
the Sequelize instance: add a beforeEach that initializes a fresh sequelize,
calls sequelize.sync({ force: true }) and ensures any models used by the tests
are (re)registered via sequelize.addModels where needed, and add an afterEach
that closes the sequelize (sequelize.close()) and clears registered models to
avoid cross-test leakage; update the top-level describe block in
encrypted-attribute.test.ts to include these beforeEach/afterEach hooks so tests
using sequelize, sequelize.addModels, and the EncryptedAttribute-decorated Model
classes run against a clean DB instance each time.

@SippieCup SippieCup self-assigned this Mar 29, 2026
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.

Introducing @EncryptedAttribute() decorator

3 participants